/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "js/GlobalObject.h" // JS_NewGlobalObject #include "js/PropertyAndElement.h" // JS_DefineProperty, JS_GetProperty, JS_SetProperty #include "js/RootingAPI.h" #include "js/SliceBudget.h" #include "jsapi-tests/tests.h" #include "vm/Compartment.h" #include "vm/Realm.h" using namespace js; static bool ConstructCCW(JSContext* cx, const JSClass* globalClasp, JS::HandleObject global1, JS::MutableHandleObject wrapper, JS::MutableHandleObject global2, JS::MutableHandleObject wrappee) { if (!global1) { fprintf(stderr, "null initial global"); return false; } // Define a second global in a different zone. JS::RealmOptions options; global2.set(JS_NewGlobalObject(cx, globalClasp, nullptr, JS::FireOnNewGlobalHook, options)); if (!global2) { fprintf(stderr, "failed to create second global"); return false; } // This should always be false, regardless. if (global1->compartment() == global2->compartment()) { fprintf(stderr, "second global claims to be in global1's compartment"); return false; } // This checks that the API obeys the implicit zone request. if (global1->zone() == global2->zone()) { fprintf(stderr, "global2 is in global1's zone"); return false; } // Define an object in compartment 2, that is wrapped by a CCW into // compartment 1. { JSAutoRealm ar(cx, global2); wrappee.set(JS_NewPlainObject(cx)); if (wrappee->compartment() != global2->compartment()) { fprintf(stderr, "wrappee in wrong compartment"); return false; } } wrapper.set(wrappee); if (!JS_WrapObject(cx, wrapper)) { fprintf(stderr, "failed to wrap"); return false; } if (wrappee == wrapper) { fprintf(stderr, "expected wrapping"); return false; } if (wrapper->compartment() != global1->compartment()) { fprintf(stderr, "wrapper in wrong compartment"); return false; } return true; } class CCWTestTracer final : public JS::CallbackTracer { void onChild(JS::GCCellPtr thing, const char* name) override { numberOfThingsTraced++; printf("*thingp = %p\n", thing.asCell()); printf("*expectedThingp = %p\n", *expectedThingp); printf("kind = %d\n", static_cast(thing.kind())); printf("expectedKind = %d\n", static_cast(expectedKind)); if (thing.asCell() != *expectedThingp || thing.kind() != expectedKind) { okay = false; } } public: bool okay; size_t numberOfThingsTraced; void** expectedThingp; JS::TraceKind expectedKind; CCWTestTracer(JSContext* cx, void** expectedThingp, JS::TraceKind expectedKind) : JS::CallbackTracer(cx), okay(true), numberOfThingsTraced(0), expectedThingp(expectedThingp), expectedKind(expectedKind) {} }; BEGIN_TEST(testTracingIncomingCCWs) { #ifdef JS_GC_ZEAL // Disable zeal modes because this test needs to control exactly when the GC // happens. JS_SetGCZeal(cx, 0, 100); #endif JS_GC(cx); JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx)); JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx)); JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx)); JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx)); CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2, &wrappee)); JS_GC(cx); CHECK(!js::gc::IsInsideNursery(wrappee)); CHECK(!js::gc::IsInsideNursery(wrapper)); JS::RootedValue v(cx, JS::ObjectValue(*wrapper)); CHECK(JS_SetProperty(cx, global1, "ccw", v)); // Ensure that |TraceIncomingCCWs| finds the object wrapped by the CCW. JS::CompartmentSet compartments; CHECK(compartments.put(global2->compartment())); void* thing = wrappee.get(); CCWTestTracer trc(cx, &thing, JS::TraceKind::Object); js::gc::TraceIncomingCCWs(&trc, compartments); CHECK(trc.numberOfThingsTraced == 1); CHECK(trc.okay); return true; } END_TEST(testTracingIncomingCCWs) static size_t countObjectWrappers(JS::Compartment* comp) { size_t count = 0; for (JS::Compartment::ObjectWrapperEnum e(comp); !e.empty(); e.popFront()) { ++count; } return count; } BEGIN_TEST(testDeadNurseryCCW) { #ifdef JS_GC_ZEAL // Disable zeal modes because this test needs to control exactly when the GC // happens. JS_SetGCZeal(cx, 0, 100); #endif JS_GC(cx); JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx)); JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx)); JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx)); JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx)); CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2, &wrappee)); CHECK(js::gc::IsInsideNursery(wrappee)); CHECK(js::gc::IsInsideNursery(wrapper)); // Now let the obj and wrapper die. wrappee = wrapper = nullptr; // Now a GC should clear the CCW. CHECK(countObjectWrappers(global1->compartment()) == 1); cx->runtime()->gc.evictNursery(); CHECK(countObjectWrappers(global1->compartment()) == 0); // Check for corruption of the CCW table by doing a full GC to force sweeping. JS_GC(cx); return true; } END_TEST(testDeadNurseryCCW) BEGIN_TEST(testLiveNurseryCCW) { #ifdef JS_GC_ZEAL // Disable zeal modes because this test needs to control exactly when the GC // happens. JS_SetGCZeal(cx, 0, 100); #endif JS_GC(cx); JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx)); JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx)); JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx)); JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx)); CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2, &wrappee)); CHECK(js::gc::IsInsideNursery(wrappee)); CHECK(js::gc::IsInsideNursery(wrapper)); // Now a GC should not kill the CCW. CHECK(countObjectWrappers(global1->compartment()) == 1); cx->runtime()->gc.evictNursery(); CHECK(countObjectWrappers(global1->compartment()) == 1); CHECK(!js::gc::IsInsideNursery(wrappee)); CHECK(!js::gc::IsInsideNursery(wrapper)); // Check for corruption of the CCW table by doing a full GC to force sweeping. JS_GC(cx); return true; } END_TEST(testLiveNurseryCCW) BEGIN_TEST(testLiveNurseryWrapperCCW) { #ifdef JS_GC_ZEAL // Disable zeal modes because this test needs to control exactly when the GC // happens. JS_SetGCZeal(cx, 0, 100); #endif JS_GC(cx); JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx)); JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx)); JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx)); JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx)); CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2, &wrappee)); CHECK(js::gc::IsInsideNursery(wrappee)); CHECK(js::gc::IsInsideNursery(wrapper)); // The wrapper contains a strong reference to the wrappee, so just dropping // the reference to the wrappee will not drop the CCW table entry as long // as the wrapper is held strongly. Thus, the minor collection here must // tenure both the wrapper and the wrappee and keep both in the table. wrappee = nullptr; // Now a GC should not kill the CCW. CHECK(countObjectWrappers(global1->compartment()) == 1); cx->runtime()->gc.evictNursery(); CHECK(countObjectWrappers(global1->compartment()) == 1); CHECK(!js::gc::IsInsideNursery(wrapper)); // Check for corruption of the CCW table by doing a full GC to force sweeping. JS_GC(cx); return true; } END_TEST(testLiveNurseryWrapperCCW) BEGIN_TEST(testLiveNurseryWrappeeCCW) { #ifdef JS_GC_ZEAL // Disable zeal modes because this test needs to control exactly when the GC // happens. JS_SetGCZeal(cx, 0, 100); #endif JS_GC(cx); JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx)); JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx)); JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx)); JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx)); CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2, &wrappee)); CHECK(js::gc::IsInsideNursery(wrappee)); CHECK(js::gc::IsInsideNursery(wrapper)); // Let the wrapper die. The wrapper should drop from the table when we GC, // even though there are other non-cross-compartment edges to it. wrapper = nullptr; // Now a GC should not kill the CCW. CHECK(countObjectWrappers(global1->compartment()) == 1); cx->runtime()->gc.evictNursery(); CHECK(countObjectWrappers(global1->compartment()) == 0); CHECK(!js::gc::IsInsideNursery(wrappee)); // Check for corruption of the CCW table by doing a full GC to force sweeping. JS_GC(cx); return true; } END_TEST(testLiveNurseryWrappeeCCW) BEGIN_TEST(testIncrementalRoots) { JSRuntime* rt = cx->runtime(); #ifdef JS_GC_ZEAL // Disable zeal modes because this test needs to control exactly when the GC // happens. JS_SetGCZeal(cx, 0, 100); #endif // Construct a big object graph to mark. In JS, the resulting object graph // is equivalent to: // // leaf = {}; // leaf2 = {}; // root = { 'obj': { 'obj': ... { 'obj': leaf, 'leaf2': leaf2 } ... } } // // with leafOwner the object that has the 'obj' and 'leaf2' properties. JS::RootedObject obj(cx, JS_NewObject(cx, nullptr)); if (!obj) { return false; } JS::RootedObject root(cx, obj); JS::RootedObject leaf(cx); JS::RootedObject leafOwner(cx); for (size_t i = 0; i < 3000; i++) { JS::RootedObject subobj(cx, JS_NewObject(cx, nullptr)); if (!subobj) { return false; } if (!JS_DefineProperty(cx, obj, "obj", subobj, 0)) { return false; } leafOwner = obj; obj = subobj; leaf = subobj; } // Give the leaf owner a second leaf. { JS::RootedObject leaf2(cx, JS_NewObject(cx, nullptr)); if (!leaf2) { return false; } if (!JS_DefineProperty(cx, leafOwner, "leaf2", leaf2, 0)) { return false; } } // This is marked during markRuntime JS::RootedObjectVector vec(cx); if (!vec.append(root)) { return false; } // Tenure everything so intentionally unrooted objects don't move before we // can use them. cx->runtime()->gc.minorGC(JS::GCReason::API); // Release all roots except for the RootedObjectVector. obj = root = nullptr; // We need to manipulate interior nodes, but the JSAPI understandably wants // to make it difficult to do that without rooting things on the stack (by // requiring Handle parameters). We can do it anyway by using // fromMarkedLocation. The hazard analysis is OK with this because the // unrooted variables are not live after they've been pointed to via // fromMarkedLocation; you're essentially lying to the analysis, saying // that the unrooted variables are rooted. // // The analysis will report this lie in its listing of "unsafe references", // but we do not break the build based on those as there are too many false // positives. JSObject* unrootedLeaf = leaf; JS::Value unrootedLeafValue = JS::ObjectValue(*leaf); JSObject* unrootedLeafOwner = leafOwner; JS::HandleObject leafHandle = JS::HandleObject::fromMarkedLocation(&unrootedLeaf); JS::HandleValue leafValueHandle = JS::HandleValue::fromMarkedLocation(&unrootedLeafValue); JS::HandleObject leafOwnerHandle = JS::HandleObject::fromMarkedLocation(&unrootedLeafOwner); leaf = leafOwner = nullptr; // Do the root marking slice. This should mark 'root' and a bunch of its // descendants. It shouldn't make it all the way through (it gets a budget // of 1000, and the graph is about 3000 objects deep). js::SliceBudget budget(js::WorkBudget(1000)); AutoGCParameter param(cx, JSGC_INCREMENTAL_GC_ENABLED, true); rt->gc.startDebugGC(JS::GCOptions::Normal, budget); while (rt->gc.state() != gc::State::Mark) { rt->gc.debugGCSlice(budget); } // We'd better be between iGC slices now. There's always a risk that // something will decide that we need to do a full GC (such as gczeal, but // that is turned off.) MOZ_ASSERT(JS::IsIncrementalGCInProgress(cx)); // And assert that the mark bits are as we expect them to be. MOZ_ASSERT(vec[0]->asTenured().isMarkedBlack()); MOZ_ASSERT(!leafHandle->asTenured().isMarkedBlack()); MOZ_ASSERT(!leafOwnerHandle->asTenured().isMarkedBlack()); #ifdef DEBUG // Remember the current GC number so we can assert that no GC occurs // between operations. auto currentGCNumber = rt->gc.gcNumber(); #endif // Now do the incremental GC's worst nightmare: rip an unmarked object // 'leaf' out of the graph and stick it into an already-marked region (hang // it off the un-prebarriered root, in fact). The pre-barrier on the // overwrite of the source location should cause this object to be marked. if (!JS_SetProperty(cx, leafOwnerHandle, "obj", JS::UndefinedHandleValue)) { return false; } MOZ_ASSERT(rt->gc.gcNumber() == currentGCNumber); if (!JS_SetProperty(cx, vec[0], "newobj", leafValueHandle)) { return false; } MOZ_ASSERT(rt->gc.gcNumber() == currentGCNumber); MOZ_ASSERT(leafHandle->asTenured().isMarkedBlack()); // Also take an unmarked object 'leaf2' from the graph and add an // additional edge from the root to it. This will not be marked by any // pre-barrier, but it is still in the live graph so it will eventually get // marked. // // Note that the root->leaf2 edge will *not* be marked through, since the // root is already marked, but that only matters if doing a compacting GC // and the compacting GC repeats the whole marking phase to update // pointers. { JS::RootedValue leaf2(cx); if (!JS_GetProperty(cx, leafOwnerHandle, "leaf2", &leaf2)) { return false; } MOZ_ASSERT(rt->gc.gcNumber() == currentGCNumber); MOZ_ASSERT(!leaf2.toObject().asTenured().isMarkedBlack()); if (!JS_SetProperty(cx, vec[0], "leafcopy", leaf2)) { return false; } MOZ_ASSERT(rt->gc.gcNumber() == currentGCNumber); MOZ_ASSERT(!leaf2.toObject().asTenured().isMarkedBlack()); } // Finish the GC using an unlimited budget. auto unlimited = js::SliceBudget::unlimited(); rt->gc.debugGCSlice(unlimited); // Access the leaf object to try to trigger a crash if it is dead. if (!JS_SetProperty(cx, leafHandle, "toes", JS::UndefinedHandleValue)) { return false; } return true; } END_TEST(testIncrementalRoots)