1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * vim: set ts=8 sts=2 et sw=2 tw=80:
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 /*
8 * GC support for FinalizationRegistry and WeakRef objects.
9 */
10
11 #include "gc/FinalizationObservers.h"
12
13 #include "mozilla/ScopeExit.h"
14
15 #include "builtin/FinalizationRegistryObject.h"
16 #include "builtin/WeakRefObject.h"
17 #include "gc/GCInternals.h"
18 #include "gc/GCRuntime.h"
19 #include "gc/Zone.h"
20 #include "vm/JSContext.h"
21
22 #include "gc/PrivateIterators-inl.h"
23 #include "gc/WeakMap-inl.h"
24 #include "vm/JSObject-inl.h"
25 #include "vm/NativeObject-inl.h"
26
27 using namespace js;
28 using namespace js::gc;
29
FinalizationObservers(Zone * zone)30 FinalizationObservers::FinalizationObservers(Zone* zone)
31 : zone(zone),
32 registries(zone),
33 recordMap(zone),
34 crossZoneRecords(zone),
35 weakRefMap(zone),
36 crossZoneWeakRefs(zone) {}
37
~FinalizationObservers()38 FinalizationObservers::~FinalizationObservers() {
39 MOZ_ASSERT(registries.empty());
40 MOZ_ASSERT(recordMap.empty());
41 MOZ_ASSERT(crossZoneRecords.empty());
42 MOZ_ASSERT(crossZoneWeakRefs.empty());
43 }
44
addFinalizationRegistry(JSContext * cx,Handle<FinalizationRegistryObject * > registry)45 bool GCRuntime::addFinalizationRegistry(
46 JSContext* cx, Handle<FinalizationRegistryObject*> registry) {
47 if (!cx->zone()->ensureFinalizationObservers() ||
48 !cx->zone()->finalizationObservers()->addRegistry(registry)) {
49 ReportOutOfMemory(cx);
50 return false;
51 }
52
53 return true;
54 }
55
addRegistry(Handle<FinalizationRegistryObject * > registry)56 bool FinalizationObservers::addRegistry(
57 Handle<FinalizationRegistryObject*> registry) {
58 return registries.put(registry);
59 }
60
registerWithFinalizationRegistry(JSContext * cx,HandleObject target,HandleObject record)61 bool GCRuntime::registerWithFinalizationRegistry(JSContext* cx,
62 HandleObject target,
63 HandleObject record) {
64 MOZ_ASSERT(!IsCrossCompartmentWrapper(target));
65 MOZ_ASSERT(
66 UncheckedUnwrapWithoutExpose(record)->is<FinalizationRecordObject>());
67 MOZ_ASSERT(target->compartment() == record->compartment());
68
69 Zone* zone = cx->zone();
70 if (!zone->ensureFinalizationObservers() ||
71 !zone->finalizationObservers()->addRecord(target, record)) {
72 ReportOutOfMemory(cx);
73 return false;
74 }
75
76 return true;
77 }
78
addRecord(HandleObject target,HandleObject record)79 bool FinalizationObservers::addRecord(HandleObject target,
80 HandleObject record) {
81 // Add a record to the record map and clean up on failure.
82 //
83 // The following must be updated and kept in sync:
84 // - the zone's recordMap (to observe the target)
85 // - the registry's global objects's recordSet (to trace the record)
86 // - the count of cross zone records (to calculate sweep groups)
87
88 MOZ_ASSERT(target->zone() == zone);
89
90 FinalizationRecordObject* unwrappedRecord =
91 &UncheckedUnwrapWithoutExpose(record)->as<FinalizationRecordObject>();
92
93 Zone* registryZone = unwrappedRecord->zone();
94 bool crossZone = registryZone != zone;
95 if (crossZone && !addCrossZoneWrapper(crossZoneRecords, record)) {
96 return false;
97 }
98 auto wrapperGuard = mozilla::MakeScopeExit([&] {
99 if (crossZone) {
100 removeCrossZoneWrapper(crossZoneRecords, record);
101 }
102 });
103
104 GlobalObject* registryGlobal = &unwrappedRecord->global();
105 auto* globalData = registryGlobal->getOrCreateFinalizationRegistryData();
106 if (!globalData || !globalData->addRecord(unwrappedRecord)) {
107 return false;
108 }
109 auto globalDataGuard = mozilla::MakeScopeExit(
110 [&] { globalData->removeRecord(unwrappedRecord); });
111
112 auto ptr = recordMap.lookupForAdd(target);
113 if (!ptr && !recordMap.add(ptr, target, RecordVector(zone))) {
114 return false;
115 }
116
117 if (!ptr->value().append(record)) {
118 return false;
119 }
120
121 unwrappedRecord->setInRecordMap(true);
122
123 globalDataGuard.release();
124 wrapperGuard.release();
125 return true;
126 }
127
addCrossZoneWrapper(WrapperWeakSet & weakSet,JSObject * wrapper)128 bool FinalizationObservers::addCrossZoneWrapper(WrapperWeakSet& weakSet,
129 JSObject* wrapper) {
130 MOZ_ASSERT(IsCrossCompartmentWrapper(wrapper));
131 MOZ_ASSERT(UncheckedUnwrapWithoutExpose(wrapper)->zone() != zone);
132
133 auto ptr = weakSet.lookupForAdd(wrapper);
134 MOZ_ASSERT(!ptr);
135 return weakSet.add(ptr, wrapper, UndefinedValue());
136 }
137
removeCrossZoneWrapper(WrapperWeakSet & weakSet,JSObject * wrapper)138 void FinalizationObservers::removeCrossZoneWrapper(WrapperWeakSet& weakSet,
139 JSObject* wrapper) {
140 MOZ_ASSERT(IsCrossCompartmentWrapper(wrapper));
141 MOZ_ASSERT(UncheckedUnwrapWithoutExpose(wrapper)->zone() != zone);
142
143 auto ptr = weakSet.lookupForAdd(wrapper);
144 MOZ_ASSERT(ptr);
145 weakSet.remove(ptr);
146 }
147
UnwrapFinalizationRecord(JSObject * obj)148 static FinalizationRecordObject* UnwrapFinalizationRecord(JSObject* obj) {
149 obj = UncheckedUnwrapWithoutExpose(obj);
150 if (!obj->is<FinalizationRecordObject>()) {
151 MOZ_ASSERT(JS_IsDeadWrapper(obj));
152 // CCWs between the compartments have been nuked. The
153 // FinalizationRegistry's callback doesn't run in this case.
154 return nullptr;
155 }
156 return &obj->as<FinalizationRecordObject>();
157 }
158
clearRecords()159 void FinalizationObservers::clearRecords() {
160 // Clear table entries related to FinalizationRecordObjects, which are not
161 // processed after the start of shutdown.
162 //
163 // WeakRefs are still updated during shutdown to avoid the possibility of
164 // stale or dangling pointers.
165
166 #ifdef DEBUG
167 checkTables();
168 #endif
169
170 recordMap.clear();
171 crossZoneRecords.clear();
172 }
173
traceWeakFinalizationObserverEdges(JSTracer * trc,Zone * zone)174 void GCRuntime::traceWeakFinalizationObserverEdges(JSTracer* trc, Zone* zone) {
175 MOZ_ASSERT(CurrentThreadCanAccessRuntime(trc->runtime()));
176 FinalizationObservers* observers = zone->finalizationObservers();
177 if (observers) {
178 observers->traceWeakEdges(trc);
179 }
180 }
181
traceRoots(JSTracer * trc)182 void FinalizationObservers::traceRoots(JSTracer* trc) {
183 // The cross-zone wrapper weak maps are traced as roots; this does not keep
184 // any of their entries alive by itself.
185 crossZoneRecords.trace(trc);
186 crossZoneWeakRefs.trace(trc);
187 }
188
traceWeakEdges(JSTracer * trc)189 void FinalizationObservers::traceWeakEdges(JSTracer* trc) {
190 traceWeakWeakRefEdges(trc);
191 traceWeakFinalizationRegistryEdges(trc);
192 }
193
traceWeakFinalizationRegistryEdges(JSTracer * trc)194 void FinalizationObservers::traceWeakFinalizationRegistryEdges(JSTracer* trc) {
195 // Sweep finalization registry data and queue finalization records for cleanup
196 // for any entries whose target is dying and remove them from the map.
197
198 GCRuntime* gc = &trc->runtime()->gc;
199
200 for (RegistrySet::Enum e(registries); !e.empty(); e.popFront()) {
201 auto result = TraceWeakEdge(trc, &e.mutableFront(), "FinalizationRegistry");
202 if (result.isDead()) {
203 auto* registry =
204 &result.initialTarget()->as<FinalizationRegistryObject>();
205 registry->queue()->setHasRegistry(false);
206 e.removeFront();
207 } else {
208 result.finalTarget()->as<FinalizationRegistryObject>().traceWeak(trc);
209 }
210 }
211
212 for (RecordMap::Enum e(recordMap); !e.empty(); e.popFront()) {
213 RecordVector& records = e.front().value();
214
215 // Sweep finalization records, updating any pointers moved by the GC and
216 // remove if necessary.
217 records.mutableEraseIf([&](HeapPtrObject& heapPtr) {
218 auto result = TraceWeakEdge(trc, &heapPtr, "FinalizationRecord");
219 JSObject* obj =
220 result.isLive() ? result.finalTarget() : result.initialTarget();
221 FinalizationRecordObject* record = UnwrapFinalizationRecord(obj);
222
223 bool shouldRemove = !result.isLive() || shouldRemoveRecord(record);
224 if (shouldRemove && record && record->isInRecordMap()) {
225 updateForRemovedRecord(obj, record);
226 }
227
228 return shouldRemove;
229 });
230
231 #ifdef DEBUG
232 for (JSObject* obj : records) {
233 MOZ_ASSERT(UnwrapFinalizationRecord(obj)->isInRecordMap());
234 }
235 #endif
236
237 // Queue finalization records for targets that are dying.
238 if (!TraceWeakEdge(trc, &e.front().mutableKey(),
239 "FinalizationRecord target")) {
240 for (JSObject* obj : records) {
241 FinalizationRecordObject* record = UnwrapFinalizationRecord(obj);
242 FinalizationQueueObject* queue = record->queue();
243 updateForRemovedRecord(obj, record);
244 queue->queueRecordToBeCleanedUp(record);
245 gc->queueFinalizationRegistryForCleanup(queue);
246 }
247 e.removeFront();
248 }
249 }
250 }
251
252 // static
shouldRemoveRecord(FinalizationRecordObject * record)253 bool FinalizationObservers::shouldRemoveRecord(
254 FinalizationRecordObject* record) {
255 // Records are removed from the target's vector for the following reasons:
256 return !record || // Nuked CCW to record.
257 !record->isRegistered() || // Unregistered record.
258 !record->queue()->hasRegistry(); // Dead finalization registry.
259 }
260
updateForRemovedRecord(JSObject * wrapper,FinalizationRecordObject * record)261 void FinalizationObservers::updateForRemovedRecord(
262 JSObject* wrapper, FinalizationRecordObject* record) {
263 // Remove other references to a record when it has been removed from the
264 // zone's record map. See addRecord().
265 MOZ_ASSERT(record->isInRecordMap());
266
267 Zone* registryZone = record->zone();
268 if (registryZone != zone) {
269 removeCrossZoneWrapper(crossZoneRecords, wrapper);
270 }
271
272 GlobalObject* registryGlobal = &record->global();
273 auto* globalData = registryGlobal->maybeFinalizationRegistryData();
274 globalData->removeRecord(record);
275
276 // The removed record may be gray, and that's OK.
277 AutoTouchingGrayThings atgt;
278
279 record->setInRecordMap(false);
280 }
281
nukeFinalizationRecordWrapper(JSObject * wrapper,FinalizationRecordObject * record)282 void GCRuntime::nukeFinalizationRecordWrapper(
283 JSObject* wrapper, FinalizationRecordObject* record) {
284 if (record->isInRecordMap()) {
285 FinalizationRegistryObject::unregisterRecord(record);
286 FinalizationObservers* observers = wrapper->zone()->finalizationObservers();
287 observers->updateForRemovedRecord(wrapper, record);
288 }
289 }
290
queueFinalizationRegistryForCleanup(FinalizationQueueObject * queue)291 void GCRuntime::queueFinalizationRegistryForCleanup(
292 FinalizationQueueObject* queue) {
293 // Prod the embedding to call us back later to run the finalization callbacks,
294 // if necessary.
295
296 if (queue->isQueuedForCleanup()) {
297 return;
298 }
299
300 // Derive the incumbent global by unwrapping the incumbent global object and
301 // then getting its global.
302 JSObject* object = UncheckedUnwrapWithoutExpose(queue->incumbentObject());
303 MOZ_ASSERT(object);
304 GlobalObject* incumbentGlobal = &object->nonCCWGlobal();
305
306 callHostCleanupFinalizationRegistryCallback(queue->doCleanupFunction(),
307 incumbentGlobal);
308
309 // The queue object may be gray, and that's OK.
310 AutoTouchingGrayThings atgt;
311
312 queue->setQueuedForCleanup(true);
313 }
314
registerWeakRef(HandleObject target,HandleObject weakRef)315 bool GCRuntime::registerWeakRef(HandleObject target, HandleObject weakRef) {
316 MOZ_ASSERT(!IsCrossCompartmentWrapper(target));
317 MOZ_ASSERT(UncheckedUnwrap(weakRef)->is<WeakRefObject>());
318 MOZ_ASSERT(target->compartment() == weakRef->compartment());
319
320 Zone* zone = target->zone();
321 return zone->ensureFinalizationObservers() &&
322 zone->finalizationObservers()->addWeakRefTarget(target, weakRef);
323 }
324
addWeakRefTarget(HandleObject target,HandleObject weakRef)325 bool FinalizationObservers::addWeakRefTarget(HandleObject target,
326 HandleObject weakRef) {
327 WeakRefObject* unwrappedWeakRef =
328 &UncheckedUnwrapWithoutExpose(weakRef)->as<WeakRefObject>();
329
330 Zone* weakRefZone = unwrappedWeakRef->zone();
331 bool crossZone = weakRefZone != zone;
332 if (crossZone && !addCrossZoneWrapper(crossZoneWeakRefs, weakRef)) {
333 return false;
334 }
335 auto wrapperGuard = mozilla::MakeScopeExit([&] {
336 if (crossZone) {
337 removeCrossZoneWrapper(crossZoneWeakRefs, weakRef);
338 }
339 });
340
341 auto ptr = weakRefMap.lookupForAdd(target);
342 if (!ptr && !weakRefMap.add(ptr, target, WeakRefHeapPtrVector(zone))) {
343 return false;
344 }
345
346 if (!ptr->value().emplaceBack(weakRef)) {
347 return false;
348 }
349
350 wrapperGuard.release();
351 return true;
352 }
353
nukeWeakRefWrapper(JSObject * wrapper,WeakRefObject * weakRef)354 void GCRuntime::nukeWeakRefWrapper(JSObject* wrapper, WeakRefObject* weakRef) {
355 // WeakRef wrappers can exist independently of the ones we create for the
356 // weakRefMap so don't assume |wrapper| is in the same zone as the WeakRef
357 // target.
358 JSObject* target = weakRef->target();
359 if (!target) {
360 return;
361 }
362
363 FinalizationObservers* observers = target->zone()->finalizationObservers();
364 if (observers) {
365 observers->unregisterWeakRefWrapper(wrapper, weakRef);
366 }
367 }
368
unregisterWeakRefWrapper(JSObject * wrapper,WeakRefObject * weakRef)369 void FinalizationObservers::unregisterWeakRefWrapper(JSObject* wrapper,
370 WeakRefObject* weakRef) {
371 JSObject* target = weakRef->target();
372 MOZ_ASSERT(target);
373
374 bool removed = false;
375 WeakRefHeapPtrVector& weakRefs = weakRefMap.lookup(target)->value();
376 weakRefs.eraseIf([wrapper, &removed](JSObject* obj) {
377 bool remove = obj == wrapper;
378 if (remove) {
379 removed = true;
380 }
381 return remove;
382 });
383
384 if (removed) {
385 updateForRemovedWeakRef(wrapper, weakRef);
386 }
387 }
388
updateForRemovedWeakRef(JSObject * wrapper,WeakRefObject * weakRef)389 void FinalizationObservers::updateForRemovedWeakRef(JSObject* wrapper,
390 WeakRefObject* weakRef) {
391 weakRef->clearTarget();
392
393 Zone* weakRefZone = weakRef->zone();
394 if (weakRefZone != zone) {
395 removeCrossZoneWrapper(crossZoneWeakRefs, wrapper);
396 }
397 }
398
UnwrapWeakRef(JSObject * obj)399 static WeakRefObject* UnwrapWeakRef(JSObject* obj) {
400 MOZ_ASSERT(!JS_IsDeadWrapper(obj));
401 obj = UncheckedUnwrapWithoutExpose(obj);
402 return &obj->as<WeakRefObject>();
403 }
404
traceWeakWeakRefEdges(JSTracer * trc)405 void FinalizationObservers::traceWeakWeakRefEdges(JSTracer* trc) {
406 for (WeakRefMap::Enum e(weakRefMap); !e.empty(); e.popFront()) {
407 // If target is dying, clear the target field of all weakRefs, and remove
408 // the entry from the map.
409 auto result = TraceWeakEdge(trc, &e.front().mutableKey(), "WeakRef target");
410 if (result.isDead()) {
411 for (JSObject* obj : e.front().value()) {
412 updateForRemovedWeakRef(obj, UnwrapWeakRef(obj));
413 }
414 e.removeFront();
415 } else {
416 // Update the target field after compacting.
417 traceWeakWeakRefVector(trc, e.front().value(), result.finalTarget());
418 }
419 }
420 }
421
traceWeakWeakRefVector(JSTracer * trc,WeakRefHeapPtrVector & weakRefs,JSObject * target)422 void FinalizationObservers::traceWeakWeakRefVector(
423 JSTracer* trc, WeakRefHeapPtrVector& weakRefs, JSObject* target) {
424 weakRefs.mutableEraseIf([&](HeapPtrObject& obj) -> bool {
425 auto result = TraceWeakEdge(trc, &obj, "WeakRef");
426 if (result.isDead()) {
427 JSObject* wrapper = result.initialTarget();
428 updateForRemovedWeakRef(wrapper, UnwrapWeakRef(wrapper));
429 } else {
430 UnwrapWeakRef(result.finalTarget())->setTargetUnbarriered(target);
431 }
432 return result.isDead();
433 });
434 }
435
436 #ifdef DEBUG
checkTables() const437 void FinalizationObservers::checkTables() const {
438 // Check all cross-zone wrappers are present in the appropriate table.
439 size_t recordCount = 0;
440 for (auto r = recordMap.all(); !r.empty(); r.popFront()) {
441 for (JSObject* object : r.front().value()) {
442 FinalizationRecordObject* record = UnwrapFinalizationRecord(object);
443 if (record && record->zone() != zone) {
444 MOZ_ASSERT(crossZoneRecords.has(object));
445 recordCount++;
446 }
447 }
448 }
449 MOZ_ASSERT(crossZoneRecords.count() == recordCount);
450
451 size_t weakRefCount = 0;
452 for (auto r = weakRefMap.all(); !r.empty(); r.popFront()) {
453 for (JSObject* object : r.front().value()) {
454 WeakRefObject* weakRef = UnwrapWeakRef(object);
455 if (weakRef && weakRef->zone() != zone) {
456 MOZ_ASSERT(crossZoneWeakRefs.has(object));
457 weakRefCount++;
458 }
459 }
460 }
461 MOZ_ASSERT(crossZoneWeakRefs.count() == weakRefCount);
462 }
463 #endif
464
FinalizationRegistryGlobalData(Zone * zone)465 FinalizationRegistryGlobalData::FinalizationRegistryGlobalData(Zone* zone)
466 : recordSet(zone) {}
467
addRecord(FinalizationRecordObject * record)468 bool FinalizationRegistryGlobalData::addRecord(
469 FinalizationRecordObject* record) {
470 return recordSet.putNew(record);
471 }
472
removeRecord(FinalizationRecordObject * record)473 void FinalizationRegistryGlobalData::removeRecord(
474 FinalizationRecordObject* record) {
475 MOZ_ASSERT_IF(!record->runtimeFromMainThread()->gc.isShuttingDown(),
476 recordSet.has(record));
477 recordSet.remove(record);
478 }
479
trace(JSTracer * trc)480 void FinalizationRegistryGlobalData::trace(JSTracer* trc) {
481 recordSet.trace(trc);
482 }
483