You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

445 lines
17 KiB

/*
* 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 */