tor-browser

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

testGCMarking.cpp (14998B)


      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 */
      4 /* This Source Code Form is subject to the terms of the Mozilla Public
      5 * License, v. 2.0. If a copy of the MPL was not distributed with this
      6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      7 
      8 #include "js/GlobalObject.h"  // JS_NewGlobalObject
      9 #include "js/PropertyAndElement.h"  // JS_DefineProperty, JS_GetProperty, JS_SetProperty
     10 #include "js/RootingAPI.h"
     11 #include "js/SliceBudget.h"
     12 #include "jsapi-tests/tests.h"
     13 #include "vm/Compartment.h"
     14 #include "vm/Realm.h"
     15 
     16 using namespace js;
     17 
     18 static bool ConstructCCW(JSContext* cx, const JSClass* globalClasp,
     19                         JS::HandleObject global1,
     20                         JS::MutableHandleObject wrapper,
     21                         JS::MutableHandleObject global2,
     22                         JS::MutableHandleObject wrappee) {
     23  if (!global1) {
     24    fprintf(stderr, "null initial global");
     25    return false;
     26  }
     27 
     28  // Define a second global in a different zone.
     29  JS::RealmOptions options;
     30  global2.set(JS_NewGlobalObject(cx, globalClasp, nullptr,
     31                                 JS::FireOnNewGlobalHook, options));
     32  if (!global2) {
     33    fprintf(stderr, "failed to create second global");
     34    return false;
     35  }
     36 
     37  // This should always be false, regardless.
     38  if (global1->compartment() == global2->compartment()) {
     39    fprintf(stderr, "second global claims to be in global1's compartment");
     40    return false;
     41  }
     42 
     43  // This checks that the API obeys the implicit zone request.
     44  if (global1->zone() == global2->zone()) {
     45    fprintf(stderr, "global2 is in global1's zone");
     46    return false;
     47  }
     48 
     49  // Define an object in compartment 2, that is wrapped by a CCW into
     50  // compartment 1.
     51  {
     52    JSAutoRealm ar(cx, global2);
     53    wrappee.set(JS_NewPlainObject(cx));
     54    if (wrappee->compartment() != global2->compartment()) {
     55      fprintf(stderr, "wrappee in wrong compartment");
     56      return false;
     57    }
     58  }
     59 
     60  wrapper.set(wrappee);
     61  if (!JS_WrapObject(cx, wrapper)) {
     62    fprintf(stderr, "failed to wrap");
     63    return false;
     64  }
     65  if (wrappee == wrapper) {
     66    fprintf(stderr, "expected wrapping");
     67    return false;
     68  }
     69  if (wrapper->compartment() != global1->compartment()) {
     70    fprintf(stderr, "wrapper in wrong compartment");
     71    return false;
     72  }
     73 
     74  return true;
     75 }
     76 
     77 class CCWTestTracer final : public JS::CallbackTracer {
     78  void onChild(JS::GCCellPtr thing, const char* name) override {
     79    numberOfThingsTraced++;
     80 
     81    printf("*thingp         = %p\n", thing.asCell());
     82    printf("*expectedThingp = %p\n", *expectedThingp);
     83 
     84    printf("kind         = %d\n", static_cast<int>(thing.kind()));
     85    printf("expectedKind = %d\n", static_cast<int>(expectedKind));
     86 
     87    if (thing.asCell() != *expectedThingp || thing.kind() != expectedKind) {
     88      okay = false;
     89    }
     90  }
     91 
     92 public:
     93  bool okay;
     94  size_t numberOfThingsTraced;
     95  void** expectedThingp;
     96  JS::TraceKind expectedKind;
     97 
     98  CCWTestTracer(JSContext* cx, void** expectedThingp,
     99                JS::TraceKind expectedKind)
    100      : JS::CallbackTracer(cx),
    101        okay(true),
    102        numberOfThingsTraced(0),
    103        expectedThingp(expectedThingp),
    104        expectedKind(expectedKind) {}
    105 };
    106 
    107 BEGIN_TEST(testTracingIncomingCCWs) {
    108 #ifdef JS_GC_ZEAL
    109  // Disable zeal modes because this test needs to control exactly when the GC
    110  // happens.
    111  JS::SetGCZeal(cx, 0, 100);
    112 #endif
    113  JS_GC(cx);
    114 
    115  JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx));
    116  JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx));
    117  JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx));
    118  JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx));
    119  CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2,
    120                     &wrappee));
    121  JS_GC(cx);
    122  CHECK(!js::gc::IsInsideNursery(wrappee));
    123  CHECK(!js::gc::IsInsideNursery(wrapper));
    124 
    125  JS::RootedValue v(cx, JS::ObjectValue(*wrapper));
    126  CHECK(JS_SetProperty(cx, global1, "ccw", v));
    127 
    128  // Ensure that |TraceIncomingCCWs| finds the object wrapped by the CCW.
    129 
    130  JS::CompartmentSet compartments;
    131  CHECK(compartments.put(global2->compartment()));
    132 
    133  void* thing = wrappee.get();
    134  CCWTestTracer trc(cx, &thing, JS::TraceKind::Object);
    135  js::gc::TraceIncomingCCWs(&trc, compartments);
    136  CHECK(trc.numberOfThingsTraced == 1);
    137  CHECK(trc.okay);
    138 
    139  return true;
    140 }
    141 END_TEST(testTracingIncomingCCWs)
    142 
    143 static size_t countObjectWrappers(JS::Compartment* comp) {
    144  size_t count = 0;
    145  for (JS::Compartment::ObjectWrapperEnum e(comp); !e.empty(); e.popFront()) {
    146    ++count;
    147  }
    148  return count;
    149 }
    150 
    151 BEGIN_TEST(testDeadNurseryCCW) {
    152 #ifdef JS_GC_ZEAL
    153  // Disable zeal modes because this test needs to control exactly when the GC
    154  // happens.
    155  JS::SetGCZeal(cx, 0, 100);
    156 #endif
    157  JS_GC(cx);
    158 
    159  JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx));
    160  JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx));
    161  JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx));
    162  JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx));
    163  CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2,
    164                     &wrappee));
    165  CHECK(js::gc::IsInsideNursery(wrappee));
    166  CHECK(js::gc::IsInsideNursery(wrapper));
    167 
    168  // Now let the obj and wrapper die.
    169  wrappee = wrapper = nullptr;
    170 
    171  // Now a GC should clear the CCW.
    172  CHECK(countObjectWrappers(global1->compartment()) == 1);
    173  cx->runtime()->gc.evictNursery();
    174  CHECK(countObjectWrappers(global1->compartment()) == 0);
    175 
    176  // Check for corruption of the CCW table by doing a full GC to force sweeping.
    177  JS_GC(cx);
    178 
    179  return true;
    180 }
    181 END_TEST(testDeadNurseryCCW)
    182 
    183 BEGIN_TEST(testLiveNurseryCCW) {
    184 #ifdef JS_GC_ZEAL
    185  // Disable zeal modes because this test needs to control exactly when the GC
    186  // happens.
    187  JS::SetGCZeal(cx, 0, 100);
    188 #endif
    189  JS_GC(cx);
    190 
    191  JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx));
    192  JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx));
    193  JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx));
    194  JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx));
    195  CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2,
    196                     &wrappee));
    197  CHECK(js::gc::IsInsideNursery(wrappee));
    198  CHECK(js::gc::IsInsideNursery(wrapper));
    199 
    200  // Now a GC should not kill the CCW.
    201  CHECK(countObjectWrappers(global1->compartment()) == 1);
    202  cx->runtime()->gc.evictNursery();
    203  CHECK(countObjectWrappers(global1->compartment()) == 1);
    204 
    205  CHECK(!js::gc::IsInsideNursery(wrappee));
    206  CHECK(!js::gc::IsInsideNursery(wrapper));
    207 
    208  // Check for corruption of the CCW table by doing a full GC to force sweeping.
    209  JS_GC(cx);
    210 
    211  return true;
    212 }
    213 END_TEST(testLiveNurseryCCW)
    214 
    215 BEGIN_TEST(testLiveNurseryWrapperCCW) {
    216 #ifdef JS_GC_ZEAL
    217  // Disable zeal modes because this test needs to control exactly when the GC
    218  // happens.
    219  JS::SetGCZeal(cx, 0, 100);
    220 #endif
    221  JS_GC(cx);
    222 
    223  JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx));
    224  JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx));
    225  JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx));
    226  JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx));
    227  CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2,
    228                     &wrappee));
    229  CHECK(js::gc::IsInsideNursery(wrappee));
    230  CHECK(js::gc::IsInsideNursery(wrapper));
    231 
    232  // The wrapper contains a strong reference to the wrappee, so just dropping
    233  // the reference to the wrappee will not drop the CCW table entry as long
    234  // as the wrapper is held strongly. Thus, the minor collection here must
    235  // tenure both the wrapper and the wrappee and keep both in the table.
    236  wrappee = nullptr;
    237 
    238  // Now a GC should not kill the CCW.
    239  CHECK(countObjectWrappers(global1->compartment()) == 1);
    240  cx->runtime()->gc.evictNursery();
    241  CHECK(countObjectWrappers(global1->compartment()) == 1);
    242 
    243  CHECK(!js::gc::IsInsideNursery(wrapper));
    244 
    245  // Check for corruption of the CCW table by doing a full GC to force sweeping.
    246  JS_GC(cx);
    247 
    248  return true;
    249 }
    250 END_TEST(testLiveNurseryWrapperCCW)
    251 
    252 BEGIN_TEST(testLiveNurseryWrappeeCCW) {
    253 #ifdef JS_GC_ZEAL
    254  // Disable zeal modes because this test needs to control exactly when the GC
    255  // happens.
    256  JS::SetGCZeal(cx, 0, 100);
    257 #endif
    258  JS_GC(cx);
    259 
    260  JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx));
    261  JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx));
    262  JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx));
    263  JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx));
    264  CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2,
    265                     &wrappee));
    266  CHECK(js::gc::IsInsideNursery(wrappee));
    267  CHECK(js::gc::IsInsideNursery(wrapper));
    268 
    269  // Let the wrapper die. The wrapper should drop from the table when we GC,
    270  // even though there are other non-cross-compartment edges to it.
    271  wrapper = nullptr;
    272 
    273  // Now a GC should not kill the CCW.
    274  CHECK(countObjectWrappers(global1->compartment()) == 1);
    275  cx->runtime()->gc.evictNursery();
    276  CHECK(countObjectWrappers(global1->compartment()) == 0);
    277 
    278  CHECK(!js::gc::IsInsideNursery(wrappee));
    279 
    280  // Check for corruption of the CCW table by doing a full GC to force sweeping.
    281  JS_GC(cx);
    282 
    283  return true;
    284 }
    285 END_TEST(testLiveNurseryWrappeeCCW)
    286 
    287 BEGIN_TEST(testIncrementalRoots) {
    288  JSRuntime* rt = cx->runtime();
    289 
    290 #ifdef JS_GC_ZEAL
    291  // Disable zeal modes because this test needs to control exactly when the GC
    292  // happens.
    293  JS::SetGCZeal(cx, 0, 100);
    294 #endif
    295 
    296  // Construct a big object graph to mark. In JS, the resulting object graph
    297  // is equivalent to:
    298  //
    299  //   leaf = {};
    300  //   leaf2 = {};
    301  //   root = { 'obj': { 'obj': ... { 'obj': leaf, 'leaf2': leaf2 } ... } }
    302  //
    303  // with leafOwner the object that has the 'obj' and 'leaf2' properties.
    304 
    305  JS::RootedObject obj(cx, JS_NewObject(cx, nullptr));
    306  if (!obj) {
    307    return false;
    308  }
    309 
    310  JS::RootedObject root(cx, obj);
    311 
    312  JS::RootedObject leaf(cx);
    313  JS::RootedObject leafOwner(cx);
    314 
    315  for (size_t i = 0; i < 3000; i++) {
    316    JS::RootedObject subobj(cx, JS_NewObject(cx, nullptr));
    317    if (!subobj) {
    318      return false;
    319    }
    320    if (!JS_DefineProperty(cx, obj, "obj", subobj, 0)) {
    321      return false;
    322    }
    323    leafOwner = obj;
    324    obj = subobj;
    325    leaf = subobj;
    326  }
    327 
    328  // Give the leaf owner a second leaf.
    329  {
    330    JS::RootedObject leaf2(cx, JS_NewObject(cx, nullptr));
    331    if (!leaf2) {
    332      return false;
    333    }
    334    if (!JS_DefineProperty(cx, leafOwner, "leaf2", leaf2, 0)) {
    335      return false;
    336    }
    337  }
    338 
    339  // This is marked during markRuntime
    340  JS::RootedObjectVector vec(cx);
    341  if (!vec.append(root)) {
    342    return false;
    343  }
    344 
    345  // Tenure everything so intentionally unrooted objects don't move before we
    346  // can use them.
    347  AutoGCParameter disableSemispace(cx, JSGC_SEMISPACE_NURSERY_ENABLED, 0);
    348  cx->runtime()->gc.minorGC(JS::GCReason::API);
    349 
    350  // Release all roots except for the RootedObjectVector.
    351  obj = root = nullptr;
    352 
    353  // We need to manipulate interior nodes, but the JSAPI understandably wants
    354  // to make it difficult to do that without rooting things on the stack (by
    355  // requiring Handle parameters). We can do it anyway by using
    356  // fromMarkedLocation. The hazard analysis is OK with this because the
    357  // unrooted variables are not live after they've been pointed to via
    358  // fromMarkedLocation; you're essentially lying to the analysis, saying
    359  // that the unrooted variables are rooted.
    360  //
    361  // The analysis will report this lie in its listing of "unsafe references",
    362  // but we do not break the build based on those as there are too many false
    363  // positives.
    364  JSObject* unrootedLeaf = leaf;
    365  JS::Value unrootedLeafValue = JS::ObjectValue(*leaf);
    366  JSObject* unrootedLeafOwner = leafOwner;
    367  JS::HandleObject leafHandle =
    368      JS::HandleObject::fromMarkedLocation(&unrootedLeaf);
    369  JS::HandleValue leafValueHandle =
    370      JS::HandleValue::fromMarkedLocation(&unrootedLeafValue);
    371  JS::HandleObject leafOwnerHandle =
    372      JS::HandleObject::fromMarkedLocation(&unrootedLeafOwner);
    373  leaf = leafOwner = nullptr;
    374 
    375  // Do the root marking slice. This should mark 'root' and a bunch of its
    376  // descendants. It shouldn't make it all the way through (it gets a budget
    377  // of 1000, and the graph is about 3000 objects deep).
    378  JS::SliceBudget budget(JS::WorkBudget(1000));
    379  AutoGCParameter param(cx, JSGC_INCREMENTAL_GC_ENABLED, true);
    380  rt->gc.startDebugGC(JS::GCOptions::Normal, budget);
    381  while (rt->gc.state() != gc::State::Mark) {
    382    rt->gc.debugGCSlice(budget);
    383  }
    384 
    385  // We'd better be between iGC slices now. There's always a risk that
    386  // something will decide that we need to do a full GC (such as gczeal, but
    387  // that is turned off.)
    388  MOZ_ASSERT(JS::IsIncrementalGCInProgress(cx));
    389 
    390  // And assert that the mark bits are as we expect them to be.
    391  MOZ_ASSERT(vec[0]->asTenured().isMarkedBlack());
    392  MOZ_ASSERT(!leafHandle->asTenured().isMarkedBlack());
    393  MOZ_ASSERT(!leafOwnerHandle->asTenured().isMarkedBlack());
    394 
    395 #ifdef DEBUG
    396  // Remember the current GC number so we can assert that no GC occurs
    397  // between operations.
    398  auto currentGCNumber = rt->gc.gcNumber();
    399 #endif
    400 
    401  // Now do the incremental GC's worst nightmare: rip an unmarked object
    402  // 'leaf' out of the graph and stick it into an already-marked region (hang
    403  // it off the un-prebarriered root, in fact). The pre-barrier on the
    404  // overwrite of the source location should cause this object to be marked.
    405  if (!JS_SetProperty(cx, leafOwnerHandle, "obj", JS::UndefinedHandleValue)) {
    406    return false;
    407  }
    408  MOZ_ASSERT(rt->gc.gcNumber() == currentGCNumber);
    409  if (!JS_SetProperty(cx, vec[0], "newobj", leafValueHandle)) {
    410    return false;
    411  }
    412  MOZ_ASSERT(rt->gc.gcNumber() == currentGCNumber);
    413  MOZ_ASSERT(leafHandle->asTenured().isMarkedBlack());
    414 
    415  // Also take an unmarked object 'leaf2' from the graph and add an
    416  // additional edge from the root to it. This will not be marked by any
    417  // pre-barrier, but it is still in the live graph so it will eventually get
    418  // marked.
    419  //
    420  // Note that the root->leaf2 edge will *not* be marked through, since the
    421  // root is already marked, but that only matters if doing a compacting GC
    422  // and the compacting GC repeats the whole marking phase to update
    423  // pointers.
    424  {
    425    JS::RootedValue leaf2(cx);
    426    if (!JS_GetProperty(cx, leafOwnerHandle, "leaf2", &leaf2)) {
    427      return false;
    428    }
    429    MOZ_ASSERT(rt->gc.gcNumber() == currentGCNumber);
    430    MOZ_ASSERT(!leaf2.toObject().asTenured().isMarkedBlack());
    431    if (!JS_SetProperty(cx, vec[0], "leafcopy", leaf2)) {
    432      return false;
    433    }
    434    MOZ_ASSERT(rt->gc.gcNumber() == currentGCNumber);
    435    MOZ_ASSERT(!leaf2.toObject().asTenured().isMarkedBlack());
    436  }
    437 
    438  // Finish the GC using an unlimited budget.
    439  auto unlimited = JS::SliceBudget::unlimited();
    440  rt->gc.debugGCSlice(unlimited);
    441 
    442  // Access the leaf object to try to trigger a crash if it is dead.
    443  if (!JS_SetProperty(cx, leafHandle, "toes", JS::UndefinedHandleValue)) {
    444    return false;
    445  }
    446 
    447  return true;
    448 }
    449 END_TEST(testIncrementalRoots)