forked from svaarala/duktape
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathduk_heap_finalize.c
448 lines (391 loc) · 16.9 KB
/
duk_heap_finalize.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
/*
* 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_context *ctx) {
DUK_DD(DUK_DDPRINT("fake global torture finalizer executed"));
/* Require a lot of stack to force a value stack grow/shrink. */
duk_require_stack(ctx, 100000);
/* Force a reallocation with pointer change for value stack
* to maximize side effects.
*/
duk_hthread_valstack_torture_realloc((duk_hthread *) ctx);
/* Inner function call, error throw. */
duk_eval_string_noresult(ctx,
"(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((duk_context *) thr, duk__fake_global_finalizer, 0 /*nargs*/);
(void) duk_pcall((duk_context *) thr, 0 /*nargs*/);
duk_pop((duk_context *) 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_context *ctx, void *udata) {
duk_hthread *thr;
DUK_ASSERT(ctx != NULL);
thr = (duk_hthread *) ctx;
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(ctx, -1, DUK_STRIDX_INT_FINALIZER); /* -> [... obj finalizer] */
duk_dup_m2(ctx);
duk_push_boolean(ctx, DUK_HEAP_HAS_FINALIZER_NORESCUE(thr->heap));
DUK_DDD(DUK_DDDPRINT("calling finalizer"));
duk_call(ctx, 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_context *ctx;
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);
ctx = (duk_context *) 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(ctx);
#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(ctx, obj); /* this also increases refcount by one */
rc = duk_safe_call(ctx, duk__finalize_helper, NULL /*udata*/, 0 /*nargs*/, 1 /*nrets*/); /* -> [... obj retval/error] */
DUK_ASSERT_TOP(ctx, 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(ctx, -1)));
}
duk_pop_2(ctx); /* -> [...] */
DUK_ASSERT_TOP(ctx, entry_top);
}
#else /* DUK_USE_FINALIZER_SUPPORT */
/* nothing */
#endif /* DUK_USE_FINALIZER_SUPPORT */