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)