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