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