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 
10 #include "js/RootingAPI.h"
11 #include "js/SliceBudget.h"
12 #include "jsapi-tests/tests.h"
13 #include "vm/JSCompartment.h"
14 
ConstructCCW(JSContext * cx,const JSClass * globalClasp,JS::HandleObject global1,JS::MutableHandleObject wrapper,JS::MutableHandleObject global2,JS::MutableHandleObject wrappee)15 static bool ConstructCCW(JSContext* cx, const JSClass* globalClasp,
16                          JS::HandleObject global1,
17                          JS::MutableHandleObject wrapper,
18                          JS::MutableHandleObject global2,
19                          JS::MutableHandleObject wrappee) {
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
47   // compartment 1.
48   {
49     JSAutoCompartment ac(cx, global2);
50     wrappee.set(JS_NewPlainObject(cx));
51     if (wrappee->compartment() != global2->compartment()) {
52       fprintf(stderr, "wrappee in wrong compartment");
53       return false;
54     }
55   }
56 
57   wrapper.set(wrappee);
58   if (!JS_WrapObject(cx, wrapper)) {
59     fprintf(stderr, "failed to wrap");
60     return false;
61   }
62   if (wrappee == wrapper) {
63     fprintf(stderr, "expected wrapping");
64     return false;
65   }
66   if (wrapper->compartment() != global1->compartment()) {
67     fprintf(stderr, "wrapper in wrong compartment");
68     return false;
69   }
70 
71   return true;
72 }
73 
74 class CCWTestTracer : public JS::CallbackTracer {
onChild(const JS::GCCellPtr & thing)75   void onChild(const JS::GCCellPtr& thing) override {
76     numberOfThingsTraced++;
77 
78     printf("*thingp         = %p\n", thing.asCell());
79     printf("*expectedThingp = %p\n", *expectedThingp);
80 
81     printf("kind         = %d\n", static_cast<int>(thing.kind()));
82     printf("expectedKind = %d\n", static_cast<int>(expectedKind));
83 
84     if (thing.asCell() != *expectedThingp || thing.kind() != expectedKind)
85       okay = false;
86   }
87 
88  public:
89   bool okay;
90   size_t numberOfThingsTraced;
91   void** expectedThingp;
92   JS::TraceKind expectedKind;
93 
CCWTestTracer(JSContext * cx,void ** expectedThingp,JS::TraceKind expectedKind)94   CCWTestTracer(JSContext* cx, void** expectedThingp,
95                 JS::TraceKind expectedKind)
96       : JS::CallbackTracer(cx),
97         okay(true),
98         numberOfThingsTraced(0),
99         expectedThingp(expectedThingp),
100         expectedKind(expectedKind) {}
101 };
102 
BEGIN_TEST(testTracingIncomingCCWs)103 BEGIN_TEST(testTracingIncomingCCWs) {
104 #ifdef JS_GC_ZEAL
105   // Disable zeal modes because this test needs to control exactly when the GC
106   // happens.
107   JS_SetGCZeal(cx, 0, 100);
108 #endif
109   JS_GC(cx);
110 
111   JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx));
112   JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx));
113   JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx));
114   JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx));
115   CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2,
116                      &wrappee));
117   JS_GC(cx);
118   CHECK(!js::gc::IsInsideNursery(wrappee));
119   CHECK(!js::gc::IsInsideNursery(wrapper));
120 
121   JS::RootedValue v(cx, JS::ObjectValue(*wrapper));
122   CHECK(JS_SetProperty(cx, global1, "ccw", v));
123 
124   // Ensure that |TraceIncomingCCWs| finds the object wrapped by the CCW.
125 
126   JS::CompartmentSet compartments;
127   CHECK(compartments.init());
128   CHECK(compartments.put(global2->compartment()));
129 
130   void* thing = wrappee.get();
131   CCWTestTracer trc(cx, &thing, JS::TraceKind::Object);
132   JS::TraceIncomingCCWs(&trc, compartments);
133   CHECK(trc.numberOfThingsTraced == 1);
134   CHECK(trc.okay);
135 
136   return true;
137 }
END_TEST(testTracingIncomingCCWs)138 END_TEST(testTracingIncomingCCWs)
139 
140 static size_t countWrappers(JSCompartment* comp) {
141   size_t count = 0;
142   for (JSCompartment::WrapperEnum e(comp); !e.empty(); e.popFront()) ++count;
143   return count;
144 }
145 
BEGIN_TEST(testDeadNurseryCCW)146 BEGIN_TEST(testDeadNurseryCCW) {
147 #ifdef JS_GC_ZEAL
148   // Disable zeal modes because this test needs to control exactly when the GC
149   // happens.
150   JS_SetGCZeal(cx, 0, 100);
151 #endif
152   JS_GC(cx);
153 
154   JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx));
155   JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx));
156   JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx));
157   JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx));
158   CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2,
159                      &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->runtime()->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 #ifdef JS_GC_ZEAL
180   // Disable zeal modes because this test needs to control exactly when the GC
181   // 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,
191                      &wrappee));
192   CHECK(js::gc::IsInsideNursery(wrappee));
193   CHECK(js::gc::IsInsideNursery(wrapper));
194 
195   // Now a GC should not kill the CCW.
196   CHECK(countWrappers(global1->compartment()) == 1);
197   cx->runtime()->gc.evictNursery();
198   CHECK(countWrappers(global1->compartment()) == 1);
199 
200   CHECK(!js::gc::IsInsideNursery(wrappee));
201   CHECK(!js::gc::IsInsideNursery(wrapper));
202 
203   // Check for corruption of the CCW table by doing a full GC to force sweeping.
204   JS_GC(cx);
205 
206   return true;
207 }
208 END_TEST(testLiveNurseryCCW)
209 
BEGIN_TEST(testLiveNurseryWrapperCCW)210 BEGIN_TEST(testLiveNurseryWrapperCCW) {
211 #ifdef JS_GC_ZEAL
212   // Disable zeal modes because this test needs to control exactly when the GC
213   // happens.
214   JS_SetGCZeal(cx, 0, 100);
215 #endif
216   JS_GC(cx);
217 
218   JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx));
219   JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx));
220   JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx));
221   JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx));
222   CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2,
223                      &wrappee));
224   CHECK(js::gc::IsInsideNursery(wrappee));
225   CHECK(js::gc::IsInsideNursery(wrapper));
226 
227   // The wrapper contains a strong reference to the wrappee, so just dropping
228   // the reference to the wrappee will not drop the CCW table entry as long
229   // as the wrapper is held strongly. Thus, the minor collection here must
230   // tenure both the wrapper and the wrappee and keep both in the table.
231   wrappee = nullptr;
232 
233   // Now a GC should not kill the CCW.
234   CHECK(countWrappers(global1->compartment()) == 1);
235   cx->runtime()->gc.evictNursery();
236   CHECK(countWrappers(global1->compartment()) == 1);
237 
238   CHECK(!js::gc::IsInsideNursery(wrapper));
239 
240   // Check for corruption of the CCW table by doing a full GC to force sweeping.
241   JS_GC(cx);
242 
243   return true;
244 }
245 END_TEST(testLiveNurseryWrapperCCW)
246 
BEGIN_TEST(testLiveNurseryWrappeeCCW)247 BEGIN_TEST(testLiveNurseryWrappeeCCW) {
248 #ifdef JS_GC_ZEAL
249   // Disable zeal modes because this test needs to control exactly when the GC
250   // happens.
251   JS_SetGCZeal(cx, 0, 100);
252 #endif
253   JS_GC(cx);
254 
255   JS::RootedObject global1(cx, JS::CurrentGlobalOrNull(cx));
256   JS::RootedObject wrapper(cx, JS::CurrentGlobalOrNull(cx));
257   JS::RootedObject global2(cx, JS::CurrentGlobalOrNull(cx));
258   JS::RootedObject wrappee(cx, JS::CurrentGlobalOrNull(cx));
259   CHECK(ConstructCCW(cx, getGlobalClass(), global1, &wrapper, &global2,
260                      &wrappee));
261   CHECK(js::gc::IsInsideNursery(wrappee));
262   CHECK(js::gc::IsInsideNursery(wrapper));
263 
264   // Let the wrapper die. The wrapper should drop from the table when we GC,
265   // even though there are other non-cross-compartment edges to it.
266   wrapper = nullptr;
267 
268   // Now a GC should not kill the CCW.
269   CHECK(countWrappers(global1->compartment()) == 1);
270   cx->runtime()->gc.evictNursery();
271   CHECK(countWrappers(global1->compartment()) == 0);
272 
273   CHECK(!js::gc::IsInsideNursery(wrappee));
274 
275   // Check for corruption of the CCW table by doing a full GC to force sweeping.
276   JS_GC(cx);
277 
278   return true;
279 }
280 END_TEST(testLiveNurseryWrappeeCCW)
281 
BEGIN_TEST(testIncrementalRoots)282 BEGIN_TEST(testIncrementalRoots) {
283   JSRuntime* rt = cx->runtime();
284 
285 #ifdef JS_GC_ZEAL
286   // Disable zeal modes because this test needs to control exactly when the GC
287   // happens.
288   JS_SetGCZeal(cx, 0, 100);
289 #endif
290 
291   // Construct a big object graph to mark. In JS, the resulting object graph
292   // is equivalent to:
293   //
294   //   leaf = {};
295   //   leaf2 = {};
296   //   root = { 'obj': { 'obj': ... { 'obj': leaf, 'leaf2': leaf2 } ... } }
297   //
298   // with leafOwner the object that has the 'obj' and 'leaf2' properties.
299 
300   JS::RootedObject obj(cx, JS_NewObject(cx, nullptr));
301   if (!obj) return false;
302 
303   JS::RootedObject root(cx, obj);
304 
305   JS::RootedObject leaf(cx);
306   JS::RootedObject leafOwner(cx);
307 
308   for (size_t i = 0; i < 3000; i++) {
309     JS::RootedObject subobj(cx, JS_NewObject(cx, nullptr));
310     if (!subobj) return false;
311     if (!JS_DefineProperty(cx, obj, "obj", subobj, 0)) 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) return false;
321     if (!JS_DefineProperty(cx, leafOwner, "leaf2", leaf2, 0)) return false;
322   }
323 
324   // This is marked during markRuntime
325   JS::AutoObjectVector vec(cx);
326   if (!vec.append(root)) return false;
327 
328   // Tenure everything so intentionally unrooted objects don't move before we
329   // can use them.
330   cx->runtime()->gc.minorGC(JS::gcreason::API);
331 
332   // Release all roots except for the AutoObjectVector.
333   obj = root = nullptr;
334 
335   // We need to manipulate interior nodes, but the JSAPI understandably wants
336   // to make it difficult to do that without rooting things on the stack (by
337   // requiring Handle parameters). We can do it anyway by using
338   // fromMarkedLocation. The hazard analysis is OK with this because the
339   // unrooted variables are not live after they've been pointed to via
340   // fromMarkedLocation; you're essentially lying to the analysis, saying
341   // that the unrooted variables are rooted.
342   //
343   // The analysis will report this lie in its listing of "unsafe references",
344   // but we do not break the build based on those as there are too many false
345   // positives.
346   JSObject* unrootedLeaf = leaf;
347   JS::Value unrootedLeafValue = JS::ObjectValue(*leaf);
348   JSObject* unrootedLeafOwner = leafOwner;
349   JS::HandleObject leafHandle =
350       JS::HandleObject::fromMarkedLocation(&unrootedLeaf);
351   JS::HandleValue leafValueHandle =
352       JS::HandleValue::fromMarkedLocation(&unrootedLeafValue);
353   JS::HandleObject leafOwnerHandle =
354       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().isMarkedBlack());
371   MOZ_ASSERT(!leafHandle->asTenured().isMarkedBlack());
372   MOZ_ASSERT(!leafOwnerHandle->asTenured().isMarkedBlack());
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)) return false;
388   MOZ_ASSERT(rt->gc.gcNumber() == currentGCNumber);
389   MOZ_ASSERT(leafHandle->asTenured().isMarkedBlack());
390 
391   // Also take an unmarked object 'leaf2' from the graph and add an
392   // additional edge from the root to it. This will not be marked by any
393   // pre-barrier, but it is still in the live graph so it will eventually get
394   // marked.
395   //
396   // Note that the root->leaf2 edge will *not* be marked through, since the
397   // root is already marked, but that only matters if doing a compacting GC
398   // and the compacting GC repeats the whole marking phase to update
399   // pointers.
400   {
401     JS::RootedValue leaf2(cx);
402     if (!JS_GetProperty(cx, leafOwnerHandle, "leaf2", &leaf2)) return false;
403     MOZ_ASSERT(rt->gc.gcNumber() == currentGCNumber);
404     MOZ_ASSERT(!leaf2.toObject().asTenured().isMarkedBlack());
405     if (!JS_SetProperty(cx, vec[0], "leafcopy", leaf2)) return false;
406     MOZ_ASSERT(rt->gc.gcNumber() == currentGCNumber);
407     MOZ_ASSERT(!leaf2.toObject().asTenured().isMarkedBlack());
408   }
409 
410   // Finish the GC using an unlimited budget.
411   auto unlimited = js::SliceBudget::unlimited();
412   rt->gc.debugGCSlice(unlimited);
413 
414   // Access the leaf object to try to trigger a crash if it is dead.
415   if (!JS_SetProperty(cx, leafHandle, "toes", JS::UndefinedHandleValue))
416     return false;
417 
418   return true;
419 }
420 END_TEST(testIncrementalRoots)
421