summaryrefslogtreecommitdiffstats
path: root/js/src/jsapi-tests/testGCMarking.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/jsapi-tests/testGCMarking.cpp')
-rw-r--r--js/src/jsapi-tests/testGCMarking.cpp448
1 files changed, 448 insertions, 0 deletions
diff --git a/js/src/jsapi-tests/testGCMarking.cpp b/js/src/jsapi-tests/testGCMarking.cpp
new file mode 100644
index 0000000000..dc2f1e0f4d
--- /dev/null
+++ b/js/src/jsapi-tests/testGCMarking.cpp
@@ -0,0 +1,448 @@
+/* -*- 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<int>(thing.kind()));
+ printf("expectedKind = %d\n", static_cast<int>(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)