/* * Finalizer handling. */ #include "duk_internal.h" #if defined(DUK_USE_FINALIZER_SUPPORT) /* * Fake torture finalizer. */ #if defined(DUK_USE_FINALIZER_TORTURE) DUK_LOCAL duk_ret_t duk__fake_global_finalizer(duk_hthread *thr) { DUK_DD(DUK_DDPRINT("fake global torture finalizer executed")); /* Require a lot of stack to force a value stack grow/shrink. */ duk_require_stack(thr, 100000); /* Force a reallocation with pointer change for value stack * to maximize side effects. */ duk_hthread_valstack_torture_realloc(thr); /* Inner function call, error throw. */ duk_eval_string_noresult(thr, "(function dummy() {\n" " dummy.prototype = null; /* break reference loop */\n" " try {\n" " throw 'fake-finalizer-dummy-error';\n" " } catch (e) {\n" " void e;\n" " }\n" "})()"); /* The above creates garbage (e.g. a function instance). Because * the function/prototype reference loop is broken, it gets collected * immediately by DECREF. If Function.prototype has a _Finalizer * property (happens in some test cases), the garbage gets queued to * finalize_list. This still won't cause an infinite loop because * the torture finalizer is called once per finalize_list run and * the garbage gets handled in the same run. (If the garbage needs * mark-and-sweep collection, an infinite loop might ensue.) */ return 0; } DUK_LOCAL void duk__run_global_torture_finalizer(duk_hthread *thr) { DUK_ASSERT(thr != NULL); /* Avoid fake finalization when callstack limit is near. Otherwise * a callstack limit error will be created, then refzero'ed. The * +5 headroom is conservative. */ if (thr->heap->call_recursion_depth + 5 >= thr->heap->call_recursion_limit || thr->callstack_top + 5 >= DUK_USE_CALLSTACK_LIMIT) { DUK_D(DUK_DPRINT("skip global torture finalizer, too little headroom for call recursion or call stack size")); return; } /* Run fake finalizer. Avoid creating unnecessary garbage. */ duk_push_c_function(thr, duk__fake_global_finalizer, 0 /*nargs*/); (void) duk_pcall(thr, 0 /*nargs*/); duk_pop(thr); } #endif /* DUK_USE_FINALIZER_TORTURE */ /* * Process the finalize_list to completion. * * An object may be placed on finalize_list by either refcounting or * mark-and-sweep. The refcount of objects placed by refcounting will be * zero; the refcount of objects placed by mark-and-sweep is > 0. In both * cases the refcount is bumped by 1 artificially so that a REFZERO event * can never happen while an object is waiting for finalization. Without * this bump a REFZERO could now happen because user code may call * duk_push_heapptr() and then pop a value even when it's on finalize_list. * * List processing assumes refcounts are kept up-to-date at all times, so * that once the finalizer returns, a zero refcount is a reliable reason to * free the object immediately rather than place it back to the heap. This * is the case because we run outside of refzero_list processing so that * DECREF cascades are handled fully inline. * * For mark-and-sweep queued objects (had_zero_refcount false) the object * may be freed immediately if its refcount is zero after the finalizer call * (i.e. finalizer removed the reference loop for the object). If not, the * next mark-and-sweep will collect the object unless it has become reachable * (i.e. rescued) by that time and its refcount hasn't fallen to zero before * that. Mark-and-sweep detects these objects because their FINALIZED flag * is set. * * There's an inherent limitation for mark-and-sweep finalizer rescuing: an * object won't get refinalized if (1) it's rescued, but (2) becomes * unreachable before mark-and-sweep has had time to notice it. The next * mark-and-sweep round simply doesn't have any information of whether the * object has been unreachable the whole time or not (the only way to get * that information would be a mark-and-sweep pass for *every finalized * object*). This is awkward for the application because the mark-and-sweep * round is not generally visible or under full application control. * * For refcount queued objects (had_zero_refcount true) the object is either * immediately freed or rescued, and waiting for a mark-and-sweep round is not * necessary (or desirable); FINALIZED is cleared when a rescued object is * queued back to heap_allocated. The object is eligible for finalization * again (either via refcounting or mark-and-sweep) immediately after being * rescued. If a refcount finalized object is placed into an unreachable * reference loop by its finalizer, it will get collected by mark-and-sweep * and currently the finalizer will execute again. * * There's a special case where: * * - Mark-and-sweep queues an object to finalize_list for finalization. * - The finalizer is executed, FINALIZED is set, and object is queued * back to heap_allocated, waiting for a new mark-and-sweep round. * - The object's refcount drops to zero before mark-and-sweep has a * chance to run another round and make a rescue/free decision. * * This is now handled by refzero code: if an object has a finalizer but * FINALIZED is already set, the object is freed without finalizer processing. * The outcome is the same as if mark-and-sweep was executed at that point; * mark-and-sweep would also free the object without another finalizer run. * This could also be changed so that the refzero-triggered finalizer *IS* * executed: being refzero collected implies someone has operated on the * object so it hasn't been totally unreachable the whole time. This would * risk a finalizer loop however. */ DUK_INTERNAL void duk_heap_process_finalize_list(duk_heap *heap) { duk_heaphdr *curr; #if defined(DUK_USE_DEBUG) duk_size_t count = 0; #endif DUK_DDD(DUK_DDDPRINT("duk_heap_process_finalize_list: %p", (void *) heap)); if (heap->pf_prevent_count != 0) { DUK_DDD(DUK_DDDPRINT("skip finalize_list processing: pf_prevent_count != 0")); return; } /* Heap alloc prevents mark-and-sweep before heap_thread is ready. */ DUK_ASSERT(heap != NULL); DUK_ASSERT(heap->heap_thread != NULL); DUK_ASSERT(heap->heap_thread->valstack != NULL); #if defined(DUK_USE_REFERENCE_COUNTING) DUK_ASSERT(heap->refzero_list == NULL); #endif DUK_ASSERT(heap->pf_prevent_count == 0); heap->pf_prevent_count = 1; /* Mark-and-sweep no longer needs to be prevented when running * finalizers: mark-and-sweep skips any rescue decisions if there * are any objects in finalize_list when mark-and-sweep is entered. * This protects finalized objects from incorrect rescue decisions * caused by finalize_list being a reachability root and only * partially processed. Freeing decisions are not postponed. */ /* When finalizer torture is enabled, make a fake finalizer call with * maximum side effects regardless of whether finalize_list is empty. */ #if defined(DUK_USE_FINALIZER_TORTURE) duk__run_global_torture_finalizer(heap->heap_thread); #endif /* Process finalize_list until it becomes empty. There's currently no * protection against a finalizer always creating more garbage. */ while ((curr = heap->finalize_list) != NULL) { #if defined(DUK_USE_REFERENCE_COUNTING) duk_bool_t queue_back; #endif DUK_DD(DUK_DDPRINT("processing finalize_list entry: %p -> %!iO", (void *) curr, curr)); DUK_ASSERT(DUK_HEAPHDR_GET_TYPE(curr) == DUK_HTYPE_OBJECT); /* Only objects have finalizers. */ DUK_ASSERT(!DUK_HEAPHDR_HAS_REACHABLE(curr)); DUK_ASSERT(!DUK_HEAPHDR_HAS_TEMPROOT(curr)); DUK_ASSERT(DUK_HEAPHDR_HAS_FINALIZABLE(curr)); /* All objects on finalize_list will have this flag (except object being finalized right now). */ DUK_ASSERT(!DUK_HEAPHDR_HAS_FINALIZED(curr)); /* Queueing code ensures. */ DUK_ASSERT(!DUK_HEAPHDR_HAS_READONLY(curr)); /* ROM objects never get freed (or finalized). */ #if defined(DUK_USE_ASSERTIONS) DUK_ASSERT(heap->currently_finalizing == NULL); heap->currently_finalizing = curr; #endif /* Clear FINALIZABLE for object being finalized, so that * duk_push_heapptr() can properly ignore the object. */ DUK_HEAPHDR_CLEAR_FINALIZABLE(curr); if (DUK_LIKELY(!heap->pf_skip_finalizers)) { /* Run the finalizer, duk_heap_run_finalizer() sets * and checks for FINALIZED to prevent the finalizer * from executing multiple times per finalization cycle. * (This safeguard shouldn't be actually needed anymore). */ #if defined(DUK_USE_REFERENCE_COUNTING) duk_bool_t had_zero_refcount; #endif /* The object's refcount is >0 throughout so it won't be * refzero processed prematurely. */ #if defined(DUK_USE_REFERENCE_COUNTING) DUK_ASSERT(DUK_HEAPHDR_GET_REFCOUNT(curr) >= 1); had_zero_refcount = (DUK_HEAPHDR_GET_REFCOUNT(curr) == 1); /* Preincremented on finalize_list insert. */ #endif DUK_ASSERT(!DUK_HEAPHDR_HAS_FINALIZED(curr)); duk_heap_run_finalizer(heap, (duk_hobject *) curr); /* must never longjmp */ DUK_ASSERT(DUK_HEAPHDR_HAS_FINALIZED(curr)); /* XXX: assert that object is still in finalize_list * when duk_push_heapptr() allows automatic rescue. */ #if defined(DUK_USE_REFERENCE_COUNTING) DUK_DD(DUK_DDPRINT("refcount after finalizer (includes bump): %ld", (long) DUK_HEAPHDR_GET_REFCOUNT(curr))); if (DUK_HEAPHDR_GET_REFCOUNT(curr) == 1) { /* Only artificial bump in refcount? */ #if defined(DUK_USE_DEBUG) if (had_zero_refcount) { DUK_DD(DUK_DDPRINT("finalized object's refcount is zero -> free immediately (refcount queued)")); } else { DUK_DD(DUK_DDPRINT("finalized object's refcount is zero -> free immediately (mark-and-sweep queued)")); } #endif queue_back = 0; } else #endif { #if defined(DUK_USE_REFERENCE_COUNTING) queue_back = 1; if (had_zero_refcount) { /* When finalization is triggered * by refzero and we queue the object * back, clear FINALIZED right away * so that the object can be refinalized * immediately if necessary. */ DUK_HEAPHDR_CLEAR_FINALIZED(curr); } #endif } } else { /* Used during heap destruction: don't actually run finalizers * because we're heading into forced finalization. Instead, * queue finalizable objects back to the heap_allocated list. */ DUK_D(DUK_DPRINT("skip finalizers flag set, queue object to heap_allocated without finalizing")); DUK_ASSERT(!DUK_HEAPHDR_HAS_FINALIZED(curr)); #if defined(DUK_USE_REFERENCE_COUNTING) queue_back = 1; #endif } /* Dequeue object from finalize_list. Note that 'curr' may no * longer be finalize_list head because new objects may have * been queued to the list. As a result we can't optimize for * the single-linked heap case and must scan the list for * removal, typically the scan is very short however. */ DUK_HEAP_REMOVE_FROM_FINALIZE_LIST(heap, curr); /* Queue back to heap_allocated or free immediately. */ #if defined(DUK_USE_REFERENCE_COUNTING) if (queue_back) { /* FINALIZED is only cleared if object originally * queued for finalization by refcounting. For * mark-and-sweep FINALIZED is left set, so that * next mark-and-sweep round can make a rescue/free * decision. */ DUK_ASSERT(DUK_HEAPHDR_GET_REFCOUNT(curr) >= 1); DUK_HEAPHDR_PREDEC_REFCOUNT(curr); /* Remove artificial refcount bump. */ DUK_HEAPHDR_CLEAR_FINALIZABLE(curr); DUK_HEAP_INSERT_INTO_HEAP_ALLOCATED(heap, curr); } else { /* No need to remove the refcount bump here. */ DUK_ASSERT(DUK_HEAPHDR_GET_TYPE(curr) == DUK_HTYPE_OBJECT); /* currently, always the case */ DUK_DD(DUK_DDPRINT("refcount finalize after finalizer call: %!O", curr)); duk_hobject_refcount_finalize_norz(heap, (duk_hobject *) curr); duk_free_hobject(heap, (duk_hobject *) curr); DUK_DD(DUK_DDPRINT("freed hobject after finalization: %p", (void *) curr)); } #else /* DUK_USE_REFERENCE_COUNTING */ DUK_HEAPHDR_CLEAR_FINALIZABLE(curr); DUK_HEAP_INSERT_INTO_HEAP_ALLOCATED(heap, curr); #endif /* DUK_USE_REFERENCE_COUNTING */ #if defined(DUK_USE_DEBUG) count++; #endif #if defined(DUK_USE_ASSERTIONS) DUK_ASSERT(heap->currently_finalizing != NULL); heap->currently_finalizing = NULL; #endif } /* finalize_list will always be processed completely. */ DUK_ASSERT(heap->finalize_list == NULL); #if 0 /* While NORZ macros are used above, this is unnecessary because the * only pending side effects are now finalizers, and finalize_list is * empty. */ DUK_REFZERO_CHECK_SLOW(heap->heap_thread); #endif /* Prevent count may be bumped while finalizers run, but should always * be reliably unbumped by the time we get here. */ DUK_ASSERT(heap->pf_prevent_count == 1); heap->pf_prevent_count = 0; #if defined(DUK_USE_DEBUG) DUK_DD(DUK_DDPRINT("duk_heap_process_finalize_list: %ld finalizers called", (long) count)); #endif } /* * Run an duk_hobject finalizer. Must never throw an uncaught error * (but may throw caught errors). * * There is no return value. Any return value or error thrown by * the finalizer is ignored (although errors are debug logged). * * Notes: * * - The finalizer thread 'top' assertions are there because it is * critical that strict stack policy is observed (i.e. no cruft * left on the finalizer stack). */ DUK_LOCAL duk_ret_t duk__finalize_helper(duk_hthread *thr, void *udata) { DUK_ASSERT(thr != NULL); DUK_UNREF(udata); DUK_DDD(DUK_DDDPRINT("protected finalization helper running")); /* [... obj] */ /* _Finalizer property is read without checking if the value is * callable or even exists. This is intentional, and handled * by throwing an error which is caught by the safe call wrapper. * * XXX: Finalizer lookup should traverse the prototype chain (to allow * inherited finalizers) but should not invoke accessors or proxy object * behavior. At the moment this lookup will invoke proxy behavior, so * caller must ensure that this function is not called if the target is * a Proxy. */ duk_get_prop_stridx_short(thr, -1, DUK_STRIDX_INT_FINALIZER); /* -> [... obj finalizer] */ duk_dup_m2(thr); duk_push_boolean(thr, DUK_HEAP_HAS_FINALIZER_NORESCUE(thr->heap)); DUK_DDD(DUK_DDDPRINT("calling finalizer")); duk_call(thr, 2); /* [ ... obj finalizer obj heapDestruct ] -> [ ... obj retval ] */ DUK_DDD(DUK_DDDPRINT("finalizer returned successfully")); return 0; /* Note: we rely on duk_safe_call() to fix up the stack for the caller, * so we don't need to pop stuff here. There is no return value; * caller determines rescued status based on object refcount. */ } DUK_INTERNAL void duk_heap_run_finalizer(duk_heap *heap, duk_hobject *obj) { duk_hthread *thr; duk_ret_t rc; #if defined(DUK_USE_ASSERTIONS) duk_idx_t entry_top; #endif DUK_DD(DUK_DDPRINT("running duk_hobject finalizer for object: %p", (void *) obj)); DUK_ASSERT(heap != NULL); DUK_ASSERT(heap->heap_thread != NULL); thr = heap->heap_thread; DUK_ASSERT(obj != NULL); DUK_ASSERT_VALSTACK_SPACE(heap->heap_thread, 1); #if defined(DUK_USE_ASSERTIONS) entry_top = duk_get_top(thr); #endif /* * Get and call the finalizer. All of this must be wrapped * in a protected call, because even getting the finalizer * may trigger an error (getter may throw one, for instance). */ /* ROM objects could inherit a finalizer, but they are never deemed * unreachable by mark-and-sweep, and their refcount never falls to 0. */ DUK_ASSERT(!DUK_HEAPHDR_HAS_READONLY((duk_heaphdr *) obj)); /* Duktape 2.1: finalize_list never contains objects with FINALIZED * set, so no need to check here. */ DUK_ASSERT(!DUK_HEAPHDR_HAS_FINALIZED((duk_heaphdr *) obj)); #if 0 if (DUK_HEAPHDR_HAS_FINALIZED((duk_heaphdr *) obj)) { DUK_D(DUK_DPRINT("object already finalized, avoid running finalizer twice: %!O", obj)); return; } #endif DUK_HEAPHDR_SET_FINALIZED((duk_heaphdr *) obj); /* ensure never re-entered until rescue cycle complete */ #if defined(DUK_USE_ES6_PROXY) if (DUK_HOBJECT_IS_PROXY(obj)) { /* This may happen if duk_set_finalizer() or Duktape.fin() is * called for a Proxy object. In such cases the fast finalizer * flag will be set on the Proxy, not the target, and neither * will be finalized. */ DUK_D(DUK_DPRINT("object is a Proxy, skip finalizer call")); return; } #endif /* DUK_USE_ES6_PROXY */ duk_push_hobject(thr, obj); /* this also increases refcount by one */ rc = duk_safe_call(thr, duk__finalize_helper, NULL /*udata*/, 0 /*nargs*/, 1 /*nrets*/); /* -> [... obj retval/error] */ DUK_ASSERT_TOP(thr, entry_top + 2); /* duk_safe_call discipline */ if (rc != DUK_EXEC_SUCCESS) { /* Note: we ask for one return value from duk_safe_call to get this * error debugging here. */ DUK_D(DUK_DPRINT("wrapped finalizer call failed for object %p (ignored); error: %!T", (void *) obj, (duk_tval *) duk_get_tval(thr, -1))); } duk_pop_2(thr); /* -> [...] */ DUK_ASSERT_TOP(thr, entry_top); } #else /* DUK_USE_FINALIZER_SUPPORT */ /* nothing */ #endif /* DUK_USE_FINALIZER_SUPPORT */