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