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