xref: /PHP-7.2/Zend/zend_gc.c (revision 589542f5)
1 /*
2    +----------------------------------------------------------------------+
3    | Zend Engine                                                          |
4    +----------------------------------------------------------------------+
5    | Copyright (c) 1998-2018 Zend Technologies Ltd. (http://www.zend.com) |
6    +----------------------------------------------------------------------+
7    | This source file is subject to version 2.00 of the Zend license,     |
8    | that is bundled with this package in the file LICENSE, and is        |
9    | available through the world-wide-web at the following url:           |
10    | http://www.zend.com/license/2_00.txt.                                |
11    | If you did not receive a copy of the Zend license and are unable to  |
12    | obtain it through the world-wide-web, please send a note to          |
13    | license@zend.com so we can mail you a copy immediately.              |
14    +----------------------------------------------------------------------+
15    | Authors: David Wang <planetbeing@gmail.com>                          |
16    |          Dmitry Stogov <dmitry@zend.com>                             |
17    +----------------------------------------------------------------------+
18 */
19 
20 /* $Id$ */
21 
22 
23 /**
24  * zend_gc_collect_cycles
25  * ======================
26  *
27  * Colors and its meaning
28  * ----------------------
29  *
30  * BLACK  (GC_BLACK)   - In use or free.
31  * GREY   (GC_GREY)    - Possible member of cycle.
32  * WHITE  (GC_WHITE)   - Member of garbage cycle.
33  * PURPLE (GC_PURPLE)  - Possible root of cycle.
34  *
35  * Colors described in the paper but not used
36  * ------------------------------------------
37  *
38  * GREEN - Acyclic
39  * RED   - Candidate cycle underogin
40  * ORANGE - Candidate cycle awaiting epoch boundary.
41  *
42  *
43  * Flow
44  * =====
45  *
46  * The garbage collect cycle starts from 'gc_mark_roots', which traverses the
47  * possible roots, and calls mark_grey for roots are marked purple with
48  * depth-first traverse.
49  *
50  * After all possible roots are traversed and marked,
51  * gc_scan_roots will be called, and each root will be called with
52  * gc_scan(root->ref)
53  *
54  * gc_scan checkes the colors of possible members.
55  *
56  * If the node is marked as grey and the refcount > 0
57  *    gc_scan_black will be called on that node to scan it's subgraph.
58  * otherwise (refcount == 0), it marks the node white.
59  *
60  * A node MAY be added to possbile roots when ZEND_UNSET_VAR happens or
61  * zend_assign_to_variable is called only when possible garbage node is
62  * produced.
63  * gc_possible_root() will be called to add the nodes to possible roots.
64  *
65  *
66  * For objects, we call their get_gc handler (by default 'zend_std_get_gc') to
67  * get the object properties to scan.
68  *
69  *
70  * @see http://researcher.watson.ibm.com/researcher/files/us-bacon/Bacon01Concurrent.pdf
71  */
72 #include "zend.h"
73 #include "zend_API.h"
74 
75 /* one (0) is reserved */
76 #define GC_ROOT_BUFFER_MAX_ENTRIES 10001
77 
78 #define GC_HAS_DESTRUCTORS  (1<<0)
79 
80 #ifndef ZEND_GC_DEBUG
81 # define ZEND_GC_DEBUG 0
82 #endif
83 
84 #ifdef ZTS
85 ZEND_API int gc_globals_id;
86 #else
87 ZEND_API zend_gc_globals gc_globals;
88 #endif
89 
90 ZEND_API int (*gc_collect_cycles)(void);
91 
92 #if ZEND_GC_DEBUG > 1
93 # define GC_TRACE(format, ...) fprintf(stderr, format "\n", ##__VA_ARGS__);
94 # define GC_TRACE_REF(ref, format, ...) \
95 	do { \
96 		gc_trace_ref((zend_refcounted *) ref); \
97 		fprintf(stderr, format "\n", ##__VA_ARGS__); \
98 	} while (0)
99 # define GC_TRACE_SET_COLOR(ref, color) \
100 	GC_TRACE_REF(ref, "->%s", gc_color_name(color))
101 #else
102 # define GC_TRACE_REF(ref, format, ...)
103 # define GC_TRACE_SET_COLOR(ref, new_color)
104 # define GC_TRACE(str)
105 #endif
106 
107 #define GC_REF_SET_ADDRESS(ref, a) \
108 	GC_INFO_SET_ADDRESS(GC_INFO(ref), a)
109 #define GC_REF_GET_COLOR(ref) \
110 	GC_INFO_GET_COLOR(GC_INFO(ref))
111 #define GC_REF_SET_COLOR(ref, c) \
112 	do { GC_TRACE_SET_COLOR(ref, c); GC_INFO_SET_COLOR(GC_INFO(ref), c); } while (0)
113 #define GC_REF_SET_BLACK(ref) \
114 	do { GC_TRACE_SET_COLOR(ref, GC_BLACK); GC_INFO_SET_BLACK(GC_INFO(ref)); } while (0)
115 #define GC_REF_SET_PURPLE(ref) \
116 	do { GC_TRACE_SET_COLOR(ref, GC_PURPLE); GC_INFO_SET_PURPLE(GC_INFO(ref)); } while (0)
117 
118 #if ZEND_GC_DEBUG > 1
gc_color_name(uint32_t color)119 static const char *gc_color_name(uint32_t color) {
120 	switch (color) {
121 		case GC_BLACK: return "black";
122 		case GC_WHITE: return "white";
123 		case GC_GREY: return "grey";
124 		case GC_PURPLE: return "purple";
125 		default: return "unknown";
126 	}
127 }
gc_trace_ref(zend_refcounted * ref)128 static void gc_trace_ref(zend_refcounted *ref) {
129 	if (GC_TYPE(ref) == IS_OBJECT) {
130 		zend_object *obj = (zend_object *) ref;
131 		fprintf(stderr, "[%p] rc=%d addr=%d %s object(%s)#%d ",
132 			ref, GC_REFCOUNT(ref), GC_ADDRESS(GC_INFO(ref)),
133 			gc_color_name(GC_REF_GET_COLOR(ref)),
134 			obj->ce->name->val, obj->handle);
135 	} else if (GC_TYPE(ref) == IS_ARRAY) {
136 		zend_array *arr = (zend_array *) ref;
137 		fprintf(stderr, "[%p] rc=%d addr=%d %s array(%d) ",
138 			ref, GC_REFCOUNT(ref), GC_ADDRESS(GC_INFO(ref)),
139 			gc_color_name(GC_REF_GET_COLOR(ref)),
140 			zend_hash_num_elements(arr));
141 	} else {
142 		fprintf(stderr, "[%p] rc=%d addr=%d %s %s ",
143 			ref, GC_REFCOUNT(ref), GC_ADDRESS(GC_INFO(ref)),
144 			gc_color_name(GC_REF_GET_COLOR(ref)),
145 			zend_get_type_by_const(GC_TYPE(ref)));
146 	}
147 }
148 #endif
149 
gc_remove_from_roots(gc_root_buffer * root)150 static zend_always_inline void gc_remove_from_roots(gc_root_buffer *root)
151 {
152 	root->next->prev = root->prev;
153 	root->prev->next = root->next;
154 	root->prev = GC_G(unused);
155 	GC_G(unused) = root;
156 	GC_BENCH_DEC(root_buf_length);
157 }
158 
gc_remove_from_additional_roots(gc_root_buffer * root)159 static zend_always_inline void gc_remove_from_additional_roots(gc_root_buffer *root)
160 {
161 	root->next->prev = root->prev;
162 	root->prev->next = root->next;
163 }
164 
root_buffer_dtor(zend_gc_globals * gc_globals)165 static void root_buffer_dtor(zend_gc_globals *gc_globals)
166 {
167 	if (gc_globals->buf) {
168 		free(gc_globals->buf);
169 		gc_globals->buf = NULL;
170 	}
171 }
172 
gc_globals_ctor_ex(zend_gc_globals * gc_globals)173 static void gc_globals_ctor_ex(zend_gc_globals *gc_globals)
174 {
175 	gc_globals->gc_enabled = 0;
176 	gc_globals->gc_active = 0;
177 
178 	gc_globals->buf = NULL;
179 
180 	gc_globals->roots.next = &gc_globals->roots;
181 	gc_globals->roots.prev = &gc_globals->roots;
182 	gc_globals->unused = NULL;
183 	gc_globals->next_to_free = NULL;
184 
185 	gc_globals->to_free.next = &gc_globals->to_free;
186 	gc_globals->to_free.prev = &gc_globals->to_free;
187 
188 	gc_globals->gc_runs = 0;
189 	gc_globals->collected = 0;
190 
191 	gc_globals->additional_buffer = NULL;
192 
193 #if GC_BENCH
194 	gc_globals->root_buf_length = 0;
195 	gc_globals->root_buf_peak = 0;
196 	gc_globals->zval_possible_root = 0;
197 	gc_globals->zval_buffered = 0;
198 	gc_globals->zval_remove_from_buffer = 0;
199 	gc_globals->zval_marked_grey = 0;
200 #endif
201 }
202 
gc_globals_ctor(void)203 ZEND_API void gc_globals_ctor(void)
204 {
205 #ifdef ZTS
206 	ts_allocate_id(&gc_globals_id, sizeof(zend_gc_globals), (ts_allocate_ctor) gc_globals_ctor_ex, (ts_allocate_dtor) root_buffer_dtor);
207 #else
208 	gc_globals_ctor_ex(&gc_globals);
209 #endif
210 }
211 
gc_globals_dtor(void)212 ZEND_API void gc_globals_dtor(void)
213 {
214 #ifndef ZTS
215 	root_buffer_dtor(&gc_globals);
216 #endif
217 }
218 
gc_reset(void)219 ZEND_API void gc_reset(void)
220 {
221 	GC_G(gc_runs) = 0;
222 	GC_G(collected) = 0;
223 	GC_G(gc_full) = 0;
224 
225 #if GC_BENCH
226 	GC_G(root_buf_length) = 0;
227 	GC_G(root_buf_peak) = 0;
228 	GC_G(zval_possible_root) = 0;
229 	GC_G(zval_buffered) = 0;
230 	GC_G(zval_remove_from_buffer) = 0;
231 	GC_G(zval_marked_grey) = 0;
232 #endif
233 
234 	GC_G(roots).next = &GC_G(roots);
235 	GC_G(roots).prev = &GC_G(roots);
236 
237 	GC_G(to_free).next = &GC_G(to_free);
238 	GC_G(to_free).prev = &GC_G(to_free);
239 
240 	if (GC_G(buf)) {
241 		GC_G(unused) = NULL;
242 		GC_G(first_unused) = GC_G(buf) + 1;
243 	} else {
244 		GC_G(unused) = NULL;
245 		GC_G(first_unused) = NULL;
246 		GC_G(last_unused) = NULL;
247 	}
248 
249 	GC_G(additional_buffer) = NULL;
250 }
251 
gc_init(void)252 ZEND_API void gc_init(void)
253 {
254 	if (GC_G(buf) == NULL && GC_G(gc_enabled)) {
255 		GC_G(buf) = (gc_root_buffer*) malloc(sizeof(gc_root_buffer) * GC_ROOT_BUFFER_MAX_ENTRIES);
256 		GC_G(last_unused) = &GC_G(buf)[GC_ROOT_BUFFER_MAX_ENTRIES];
257 		gc_reset();
258 	}
259 }
260 
gc_possible_root(zend_refcounted * ref)261 ZEND_API void ZEND_FASTCALL gc_possible_root(zend_refcounted *ref)
262 {
263 	gc_root_buffer *newRoot;
264 
265 	if (UNEXPECTED(CG(unclean_shutdown)) || UNEXPECTED(GC_G(gc_active))) {
266 		return;
267 	}
268 
269 	ZEND_ASSERT(GC_TYPE(ref) == IS_ARRAY || GC_TYPE(ref) == IS_OBJECT);
270 	ZEND_ASSERT(EXPECTED(GC_REF_GET_COLOR(ref) == GC_BLACK));
271 	ZEND_ASSERT(!GC_ADDRESS(GC_INFO(ref)));
272 
273 	GC_BENCH_INC(zval_possible_root);
274 
275 	newRoot = GC_G(unused);
276 	if (newRoot) {
277 		GC_G(unused) = newRoot->prev;
278 	} else if (GC_G(first_unused) != GC_G(last_unused)) {
279 		newRoot = GC_G(first_unused);
280 		GC_G(first_unused)++;
281 	} else {
282 		if (!GC_G(gc_enabled)) {
283 			return;
284 		}
285 		GC_REFCOUNT(ref)++;
286 		gc_collect_cycles();
287 		GC_REFCOUNT(ref)--;
288 		if (UNEXPECTED(GC_REFCOUNT(ref)) == 0) {
289 			zval_dtor_func(ref);
290 			return;
291 		}
292 		if (UNEXPECTED(GC_INFO(ref))) {
293 			return;
294 		}
295 		newRoot = GC_G(unused);
296 		if (!newRoot) {
297 #if ZEND_GC_DEBUG
298 			if (!GC_G(gc_full)) {
299 				fprintf(stderr, "GC: no space to record new root candidate\n");
300 				GC_G(gc_full) = 1;
301 			}
302 #endif
303 			return;
304 		}
305 		GC_G(unused) = newRoot->prev;
306 	}
307 
308 	GC_TRACE_SET_COLOR(ref, GC_PURPLE);
309 	GC_INFO(ref) = (newRoot - GC_G(buf)) | GC_PURPLE;
310 	newRoot->ref = ref;
311 
312 	newRoot->next = GC_G(roots).next;
313 	newRoot->prev = &GC_G(roots);
314 	GC_G(roots).next->prev = newRoot;
315 	GC_G(roots).next = newRoot;
316 
317 	GC_BENCH_INC(zval_buffered);
318 	GC_BENCH_INC(root_buf_length);
319 	GC_BENCH_PEAK(root_buf_peak, root_buf_length);
320 }
321 
gc_find_additional_buffer(zend_refcounted * ref)322 static zend_always_inline gc_root_buffer* gc_find_additional_buffer(zend_refcounted *ref)
323 {
324 	gc_additional_buffer *additional_buffer = GC_G(additional_buffer);
325 
326 	/* We have to check each additional_buffer to find which one holds the ref */
327 	while (additional_buffer) {
328 		uint32_t idx = GC_ADDRESS(GC_INFO(ref)) - GC_ROOT_BUFFER_MAX_ENTRIES;
329 		if (idx < additional_buffer->used) {
330 			gc_root_buffer *root = additional_buffer->buf + idx;
331 			if (root->ref == ref) {
332 				return root;
333 			}
334 		}
335 		additional_buffer = additional_buffer->next;
336 	}
337 
338 	ZEND_ASSERT(0);
339 	return NULL;
340 }
341 
gc_remove_from_buffer(zend_refcounted * ref)342 ZEND_API void ZEND_FASTCALL gc_remove_from_buffer(zend_refcounted *ref)
343 {
344 	gc_root_buffer *root;
345 
346 	ZEND_ASSERT(GC_ADDRESS(GC_INFO(ref)));
347 
348 	GC_BENCH_INC(zval_remove_from_buffer);
349 
350 	if (EXPECTED(GC_ADDRESS(GC_INFO(ref)) < GC_ROOT_BUFFER_MAX_ENTRIES)) {
351 		root = GC_G(buf) + GC_ADDRESS(GC_INFO(ref));
352 		gc_remove_from_roots(root);
353 	} else {
354 		root = gc_find_additional_buffer(ref);
355 		gc_remove_from_additional_roots(root);
356 	}
357 	if (GC_REF_GET_COLOR(ref) != GC_BLACK) {
358 		GC_TRACE_SET_COLOR(ref, GC_PURPLE);
359 	}
360 	GC_INFO(ref) = 0;
361 
362 	/* updete next root that is going to be freed */
363 	if (GC_G(next_to_free) == root) {
364 		GC_G(next_to_free) = root->next;
365 	}
366 }
367 
gc_scan_black(zend_refcounted * ref)368 static void gc_scan_black(zend_refcounted *ref)
369 {
370 	HashTable *ht;
371 	Bucket *p, *end;
372 	zval *zv;
373 
374 tail_call:
375 	ht = NULL;
376 	GC_REF_SET_BLACK(ref);
377 
378 	if (GC_TYPE(ref) == IS_OBJECT) {
379 		zend_object_get_gc_t get_gc;
380 		zend_object *obj = (zend_object*)ref;
381 
382 		if (EXPECTED(!(GC_FLAGS(ref) & IS_OBJ_FREE_CALLED) &&
383 		             (get_gc = obj->handlers->get_gc) != NULL)) {
384 			int n;
385 			zval *zv, *end;
386 			zval tmp;
387 
388 			ZVAL_OBJ(&tmp, obj);
389 			ht = get_gc(&tmp, &zv, &n);
390 			end = zv + n;
391 			if (EXPECTED(!ht) || UNEXPECTED(GC_REF_GET_COLOR(ht) == GC_BLACK)) {
392 				ht = NULL;
393 				if (!n) return;
394 				while (!Z_REFCOUNTED_P(--end)) {
395 					if (zv == end) return;
396 				}
397 			} else {
398 				GC_REF_SET_BLACK(ht);
399 			}
400 			while (zv != end) {
401 				if (Z_REFCOUNTED_P(zv)) {
402 					ref = Z_COUNTED_P(zv);
403 					GC_REFCOUNT(ref)++;
404 					if (GC_REF_GET_COLOR(ref) != GC_BLACK) {
405 						gc_scan_black(ref);
406 					}
407 				}
408 				zv++;
409 			}
410 			if (EXPECTED(!ht)) {
411 				ref = Z_COUNTED_P(zv);
412 				GC_REFCOUNT(ref)++;
413 				if (GC_REF_GET_COLOR(ref) != GC_BLACK) {
414 					goto tail_call;
415 				}
416 				return;
417 			}
418 		} else {
419 			return;
420 		}
421 	} else if (GC_TYPE(ref) == IS_ARRAY) {
422 		if ((zend_array*)ref != &EG(symbol_table)) {
423 			ht = (zend_array*)ref;
424 		} else {
425 			return;
426 		}
427 	} else if (GC_TYPE(ref) == IS_REFERENCE) {
428 		if (Z_REFCOUNTED(((zend_reference*)ref)->val)) {
429 			ref = Z_COUNTED(((zend_reference*)ref)->val);
430 			GC_REFCOUNT(ref)++;
431 			if (GC_REF_GET_COLOR(ref) != GC_BLACK) {
432 				goto tail_call;
433 			}
434 		}
435 		return;
436 	} else {
437 		return;
438 	}
439 
440 	if (!ht->nNumUsed) return;
441 	p = ht->arData;
442 	end = p + ht->nNumUsed;
443 	while (1) {
444 		end--;
445 		zv = &end->val;
446 		if (Z_TYPE_P(zv) == IS_INDIRECT) {
447 			zv = Z_INDIRECT_P(zv);
448 		}
449 		if (Z_REFCOUNTED_P(zv)) {
450 			break;
451 		}
452 		if (p == end) return;
453 	}
454 	while (p != end) {
455 		zv = &p->val;
456 		if (Z_TYPE_P(zv) == IS_INDIRECT) {
457 			zv = Z_INDIRECT_P(zv);
458 		}
459 		if (Z_REFCOUNTED_P(zv)) {
460 			ref = Z_COUNTED_P(zv);
461 			GC_REFCOUNT(ref)++;
462 			if (GC_REF_GET_COLOR(ref) != GC_BLACK) {
463 				gc_scan_black(ref);
464 			}
465 		}
466 		p++;
467 	}
468 	zv = &p->val;
469 	if (Z_TYPE_P(zv) == IS_INDIRECT) {
470 		zv = Z_INDIRECT_P(zv);
471 	}
472 	ref = Z_COUNTED_P(zv);
473 	GC_REFCOUNT(ref)++;
474 	if (GC_REF_GET_COLOR(ref) != GC_BLACK) {
475 		goto tail_call;
476 	}
477 }
478 
gc_mark_grey(zend_refcounted * ref)479 static void gc_mark_grey(zend_refcounted *ref)
480 {
481     HashTable *ht;
482 	Bucket *p, *end;
483 	zval *zv;
484 
485 tail_call:
486 	if (GC_REF_GET_COLOR(ref) != GC_GREY) {
487 		ht = NULL;
488 		GC_BENCH_INC(zval_marked_grey);
489 		GC_REF_SET_COLOR(ref, GC_GREY);
490 
491 		if (GC_TYPE(ref) == IS_OBJECT) {
492 			zend_object_get_gc_t get_gc;
493 			zend_object *obj = (zend_object*)ref;
494 
495 			if (EXPECTED(!(GC_FLAGS(ref) & IS_OBJ_FREE_CALLED) &&
496 		                 (get_gc = obj->handlers->get_gc) != NULL)) {
497 				int n;
498 				zval *zv, *end;
499 				zval tmp;
500 
501 				ZVAL_OBJ(&tmp, obj);
502 				ht = get_gc(&tmp, &zv, &n);
503 				end = zv + n;
504 				if (EXPECTED(!ht) || UNEXPECTED(GC_REF_GET_COLOR(ht) == GC_GREY)) {
505 					ht = NULL;
506 					if (!n) return;
507 					while (!Z_REFCOUNTED_P(--end)) {
508 						if (zv == end) return;
509 					}
510 				} else {
511 					GC_REF_SET_COLOR(ht, GC_GREY);
512 				}
513 				while (zv != end) {
514 					if (Z_REFCOUNTED_P(zv)) {
515 						ref = Z_COUNTED_P(zv);
516 						ZEND_ASSERT(GC_REFCOUNT(ref) > 0);
517 						GC_REFCOUNT(ref)--;
518 						gc_mark_grey(ref);
519 					}
520 					zv++;
521 				}
522 				if (EXPECTED(!ht)) {
523 					ref = Z_COUNTED_P(zv);
524 					ZEND_ASSERT(GC_REFCOUNT(ref) > 0);
525 					GC_REFCOUNT(ref)--;
526 					goto tail_call;
527 				}
528 			} else {
529 				return;
530 			}
531 		} else if (GC_TYPE(ref) == IS_ARRAY) {
532 			if (((zend_array*)ref) == &EG(symbol_table)) {
533 				GC_REF_SET_BLACK(ref);
534 				return;
535 			} else {
536 				ht = (zend_array*)ref;
537 			}
538 		} else if (GC_TYPE(ref) == IS_REFERENCE) {
539 			if (Z_REFCOUNTED(((zend_reference*)ref)->val)) {
540 				ref = Z_COUNTED(((zend_reference*)ref)->val);
541 				ZEND_ASSERT(GC_REFCOUNT(ref) > 0);
542 				GC_REFCOUNT(ref)--;
543 				goto tail_call;
544 			}
545 			return;
546 		} else {
547 			return;
548 		}
549 
550 		if (!ht->nNumUsed) return;
551 		p = ht->arData;
552 		end = p + ht->nNumUsed;
553 		while (1) {
554 			end--;
555 			zv = &end->val;
556 			if (Z_TYPE_P(zv) == IS_INDIRECT) {
557 				zv = Z_INDIRECT_P(zv);
558 			}
559 			if (Z_REFCOUNTED_P(zv)) {
560 				break;
561 			}
562 			if (p == end) return;
563 		}
564 		while (p != end) {
565 			zv = &p->val;
566 			if (Z_TYPE_P(zv) == IS_INDIRECT) {
567 				zv = Z_INDIRECT_P(zv);
568 			}
569 			if (Z_REFCOUNTED_P(zv)) {
570 				ref = Z_COUNTED_P(zv);
571 				ZEND_ASSERT(GC_REFCOUNT(ref) > 0);
572 				GC_REFCOUNT(ref)--;
573 				gc_mark_grey(ref);
574 			}
575 			p++;
576 		}
577 		zv = &p->val;
578 		if (Z_TYPE_P(zv) == IS_INDIRECT) {
579 			zv = Z_INDIRECT_P(zv);
580 		}
581 		ref = Z_COUNTED_P(zv);
582 		ZEND_ASSERT(GC_REFCOUNT(ref) > 0);
583 		GC_REFCOUNT(ref)--;
584 		goto tail_call;
585 	}
586 }
587 
gc_mark_roots(void)588 static void gc_mark_roots(void)
589 {
590 	gc_root_buffer *current = GC_G(roots).next;
591 
592 	while (current != &GC_G(roots)) {
593 		if (GC_REF_GET_COLOR(current->ref) == GC_PURPLE) {
594 			gc_mark_grey(current->ref);
595 		}
596 		current = current->next;
597 	}
598 }
599 
gc_scan(zend_refcounted * ref)600 static void gc_scan(zend_refcounted *ref)
601 {
602     HashTable *ht;
603 	Bucket *p, *end;
604 	zval *zv;
605 
606 tail_call:
607 	if (GC_REF_GET_COLOR(ref) == GC_GREY) {
608 		if (GC_REFCOUNT(ref) > 0) {
609 			gc_scan_black(ref);
610 		} else {
611 			GC_REF_SET_COLOR(ref, GC_WHITE);
612 			if (GC_TYPE(ref) == IS_OBJECT) {
613 				zend_object_get_gc_t get_gc;
614 				zend_object *obj = (zend_object*)ref;
615 
616 				if (EXPECTED(!(GC_FLAGS(ref) & IS_OBJ_FREE_CALLED) &&
617 				             (get_gc = obj->handlers->get_gc) != NULL)) {
618 					int n;
619 					zval *zv, *end;
620 					zval tmp;
621 
622 					ZVAL_OBJ(&tmp, obj);
623 					ht = get_gc(&tmp, &zv, &n);
624 					end = zv + n;
625 					if (EXPECTED(!ht) || UNEXPECTED(GC_REF_GET_COLOR(ht) != GC_GREY)) {
626 						ht = NULL;
627 						if (!n) return;
628 						while (!Z_REFCOUNTED_P(--end)) {
629 							if (zv == end) return;
630 						}
631 					} else {
632 						GC_REF_SET_COLOR(ht, GC_WHITE);
633 					}
634 					while (zv != end) {
635 						if (Z_REFCOUNTED_P(zv)) {
636 							ref = Z_COUNTED_P(zv);
637 							gc_scan(ref);
638 						}
639 						zv++;
640 					}
641 					if (EXPECTED(!ht)) {
642 						ref = Z_COUNTED_P(zv);
643 						goto tail_call;
644 					}
645 				} else {
646 					return;
647 				}
648 			} else if (GC_TYPE(ref) == IS_ARRAY) {
649 				if ((zend_array*)ref == &EG(symbol_table)) {
650 					GC_REF_SET_BLACK(ref);
651 					return;
652 				} else {
653 					ht = (zend_array*)ref;
654 				}
655 			} else if (GC_TYPE(ref) == IS_REFERENCE) {
656 				if (Z_REFCOUNTED(((zend_reference*)ref)->val)) {
657 					ref = Z_COUNTED(((zend_reference*)ref)->val);
658 					goto tail_call;
659 				}
660 				return;
661 			} else {
662 				return;
663 			}
664 
665 			if (!ht->nNumUsed) return;
666 			p = ht->arData;
667 			end = p + ht->nNumUsed;
668 			while (1) {
669 				end--;
670 				zv = &end->val;
671 				if (Z_TYPE_P(zv) == IS_INDIRECT) {
672 					zv = Z_INDIRECT_P(zv);
673 				}
674 				if (Z_REFCOUNTED_P(zv)) {
675 					break;
676 				}
677 				if (p == end) return;
678 			}
679 			while (p != end) {
680 				zv = &p->val;
681 				if (Z_TYPE_P(zv) == IS_INDIRECT) {
682 					zv = Z_INDIRECT_P(zv);
683 				}
684 				if (Z_REFCOUNTED_P(zv)) {
685 					ref = Z_COUNTED_P(zv);
686 					gc_scan(ref);
687 				}
688 				p++;
689 			}
690 			zv = &p->val;
691 			if (Z_TYPE_P(zv) == IS_INDIRECT) {
692 				zv = Z_INDIRECT_P(zv);
693 			}
694 			ref = Z_COUNTED_P(zv);
695 			goto tail_call;
696 		}
697 	}
698 }
699 
gc_scan_roots(void)700 static void gc_scan_roots(void)
701 {
702 	gc_root_buffer *current = GC_G(roots).next;
703 
704 	while (current != &GC_G(roots)) {
705 		gc_scan(current->ref);
706 		current = current->next;
707 	}
708 }
709 
gc_add_garbage(zend_refcounted * ref)710 static void gc_add_garbage(zend_refcounted *ref)
711 {
712 	gc_root_buffer *buf = GC_G(unused);
713 
714 	if (buf) {
715 		GC_G(unused) = buf->prev;
716 #if 1
717 		/* optimization: color is already GC_BLACK (0) */
718 		GC_INFO(ref) = buf - GC_G(buf);
719 #else
720 		GC_REF_SET_ADDRESS(ref, buf - GC_G(buf));
721 #endif
722 	} else if (GC_G(first_unused) != GC_G(last_unused)) {
723 		buf = GC_G(first_unused);
724 		GC_G(first_unused)++;
725 #if 1
726 		/* optimization: color is already GC_BLACK (0) */
727 		GC_INFO(ref) = buf - GC_G(buf);
728 #else
729 		GC_REF_SET_ADDRESS(ref, buf - GC_G(buf));
730 #endif
731 	} else {
732 		/* If we don't have free slots in the buffer, allocate a new one and
733 		 * set it's address above GC_ROOT_BUFFER_MAX_ENTRIES that have special
734 		 * meaning.
735 		 */
736 		if (!GC_G(additional_buffer) || GC_G(additional_buffer)->used == GC_NUM_ADDITIONAL_ENTRIES) {
737 			gc_additional_buffer *new_buffer = emalloc(sizeof(gc_additional_buffer));
738 			new_buffer->used = 0;
739 			new_buffer->next = GC_G(additional_buffer);
740 			GC_G(additional_buffer) = new_buffer;
741 		}
742 		buf = GC_G(additional_buffer)->buf + GC_G(additional_buffer)->used;
743 #if 1
744 		/* optimization: color is already GC_BLACK (0) */
745 		GC_INFO(ref) = GC_ROOT_BUFFER_MAX_ENTRIES + GC_G(additional_buffer)->used;
746 #else
747 		GC_REF_SET_ADDRESS(ref, GC_ROOT_BUFFER_MAX_ENTRIES) + GC_G(additional_buffer)->used;
748 #endif
749 		GC_G(additional_buffer)->used++;
750 	}
751 	if (buf) {
752 		buf->ref = ref;
753 		buf->next = GC_G(roots).next;
754 		buf->prev = &GC_G(roots);
755 		GC_G(roots).next->prev = buf;
756 		GC_G(roots).next = buf;
757 	}
758 }
759 
gc_collect_white(zend_refcounted * ref,uint32_t * flags)760 static int gc_collect_white(zend_refcounted *ref, uint32_t *flags)
761 {
762 	int count = 0;
763 	HashTable *ht;
764 	Bucket *p, *end;
765 	zval *zv;
766 
767 tail_call:
768 	if (GC_REF_GET_COLOR(ref) == GC_WHITE) {
769 		ht = NULL;
770 		GC_REF_SET_BLACK(ref);
771 
772 		/* don't count references for compatibility ??? */
773 		if (GC_TYPE(ref) != IS_REFERENCE) {
774 			count++;
775 		}
776 
777 		if (GC_TYPE(ref) == IS_OBJECT) {
778 			zend_object_get_gc_t get_gc;
779 			zend_object *obj = (zend_object*)ref;
780 
781 			if (EXPECTED(!(GC_FLAGS(ref) & IS_OBJ_FREE_CALLED) &&
782 			             (get_gc = obj->handlers->get_gc) != NULL)) {
783 				int n;
784 				zval *zv, *end;
785 				zval tmp;
786 
787 #if 1
788 				/* optimization: color is GC_BLACK (0) */
789 				if (!GC_INFO(ref)) {
790 #else
791 				if (!GC_ADDRESS(GC_INFO(ref))) {
792 #endif
793 					gc_add_garbage(ref);
794 				}
795 				if (obj->handlers->dtor_obj &&
796 				    ((obj->handlers->dtor_obj != zend_objects_destroy_object) ||
797 				     (obj->ce->destructor != NULL))) {
798 					*flags |= GC_HAS_DESTRUCTORS;
799 				}
800 				ZVAL_OBJ(&tmp, obj);
801 				ht = get_gc(&tmp, &zv, &n);
802 				end = zv + n;
803 				if (EXPECTED(!ht) || UNEXPECTED(GC_REF_GET_COLOR(ht) == GC_BLACK)) {
804 					ht = NULL;
805 					if (!n) return count;
806 					while (!Z_REFCOUNTED_P(--end)) {
807 						/* count non-refcounted for compatibility ??? */
808 						if (Z_TYPE_P(zv) != IS_UNDEF) {
809 							count++;
810 						}
811 						if (zv == end) return count;
812 					}
813 				} else {
814 					GC_REF_SET_BLACK(ht);
815 				}
816 				while (zv != end) {
817 					if (Z_REFCOUNTED_P(zv)) {
818 						ref = Z_COUNTED_P(zv);
819 						GC_REFCOUNT(ref)++;
820 						count += gc_collect_white(ref, flags);
821 					/* count non-refcounted for compatibility ??? */
822 					} else if (Z_TYPE_P(zv) != IS_UNDEF) {
823 						count++;
824 					}
825 					zv++;
826 				}
827 				if (EXPECTED(!ht)) {
828 					ref = Z_COUNTED_P(zv);
829 					GC_REFCOUNT(ref)++;
830 					goto tail_call;
831 				}
832 			} else {
833 				return count;
834 			}
835 		} else if (GC_TYPE(ref) == IS_ARRAY) {
836 #if 1
837 				/* optimization: color is GC_BLACK (0) */
838 				if (!GC_INFO(ref)) {
839 #else
840 				if (!GC_ADDRESS(GC_INFO(ref))) {
841 #endif
842 				gc_add_garbage(ref);
843 			}
844 			ht = (zend_array*)ref;
845 		} else if (GC_TYPE(ref) == IS_REFERENCE) {
846 			if (Z_REFCOUNTED(((zend_reference*)ref)->val)) {
847 				ref = Z_COUNTED(((zend_reference*)ref)->val);
848 				GC_REFCOUNT(ref)++;
849 				goto tail_call;
850 			}
851 			return count;
852 		} else {
853 			return count;
854 		}
855 
856 		if (!ht->nNumUsed) return count;
857 		p = ht->arData;
858 		end = p + ht->nNumUsed;
859 		while (1) {
860 			end--;
861 			zv = &end->val;
862 			if (Z_TYPE_P(zv) == IS_INDIRECT) {
863 				zv = Z_INDIRECT_P(zv);
864 			}
865 			if (Z_REFCOUNTED_P(zv)) {
866 				break;
867 			}
868 			/* count non-refcounted for compatibility ??? */
869 			if (Z_TYPE_P(zv) != IS_UNDEF) {
870 				count++;
871 			}
872 			if (p == end) return count;
873 		}
874 		while (p != end) {
875 			zv = &p->val;
876 			if (Z_TYPE_P(zv) == IS_INDIRECT) {
877 				zv = Z_INDIRECT_P(zv);
878 			}
879 			if (Z_REFCOUNTED_P(zv)) {
880 				ref = Z_COUNTED_P(zv);
881 				GC_REFCOUNT(ref)++;
882 				count += gc_collect_white(ref, flags);
883 				/* count non-refcounted for compatibility ??? */
884 			} else if (Z_TYPE_P(zv) != IS_UNDEF) {
885 				count++;
886 			}
887 			p++;
888 		}
889 		zv = &p->val;
890 		if (Z_TYPE_P(zv) == IS_INDIRECT) {
891 			zv = Z_INDIRECT_P(zv);
892 		}
893 		ref = Z_COUNTED_P(zv);
894 		GC_REFCOUNT(ref)++;
895 		goto tail_call;
896 	}
897 	return count;
898 }
899 
900 static int gc_collect_roots(uint32_t *flags)
901 {
902 	int count = 0;
903 	gc_root_buffer *current = GC_G(roots).next;
904 
905 	/* remove non-garbage from the list */
906 	while (current != &GC_G(roots)) {
907 		gc_root_buffer *next = current->next;
908 		if (GC_REF_GET_COLOR(current->ref) == GC_BLACK) {
909 			if (EXPECTED(GC_ADDRESS(GC_INFO(current->ref)) < GC_ROOT_BUFFER_MAX_ENTRIES)) {
910 				gc_remove_from_roots(current);
911 			} else {
912 				gc_remove_from_additional_roots(current);
913 			}
914 			GC_INFO(current->ref) = 0; /* reset GC_ADDRESS() and keep GC_BLACK */
915 		}
916 		current = next;
917 	}
918 
919 	current = GC_G(roots).next;
920 	while (current != &GC_G(roots)) {
921 		if (GC_REF_GET_COLOR(current->ref) == GC_WHITE) {
922 			count += gc_collect_white(current->ref, flags);
923 		}
924 		current = current->next;
925 	}
926 
927 	/* relink remaining roots into list to free */
928 	if (GC_G(roots).next != &GC_G(roots)) {
929 		if (GC_G(to_free).next == &GC_G(to_free)) {
930 			/* move roots into list to free */
931 			GC_G(to_free).next = GC_G(roots).next;
932 			GC_G(to_free).prev = GC_G(roots).prev;
933 			GC_G(to_free).next->prev = &GC_G(to_free);
934 			GC_G(to_free).prev->next = &GC_G(to_free);
935 		} else {
936 			/* add roots into list to free */
937 			GC_G(to_free).prev->next = GC_G(roots).next;
938 			GC_G(roots).next->prev = GC_G(to_free).prev;
939 			GC_G(roots).prev->next = &GC_G(to_free);
940 			GC_G(to_free).prev = GC_G(roots).prev;
941 		}
942 
943 		GC_G(roots).next = &GC_G(roots);
944 		GC_G(roots).prev = &GC_G(roots);
945 	}
946 	return count;
947 }
948 
949 static void gc_remove_nested_data_from_buffer(zend_refcounted *ref, gc_root_buffer *root)
950 {
951 	HashTable *ht = NULL;
952 	Bucket *p, *end;
953 	zval *zv;
954 
955 tail_call:
956 	do {
957 		if (root) {
958 			GC_TRACE_REF(ref, "removing from buffer");
959 			if (EXPECTED(GC_ADDRESS(GC_INFO(root->ref)) < GC_ROOT_BUFFER_MAX_ENTRIES)) {
960 				gc_remove_from_roots(root);
961 			} else {
962 				gc_remove_from_additional_roots(root);
963 			}
964 			GC_INFO(ref) = 0;
965 			root = NULL;
966 		} else if (GC_ADDRESS(GC_INFO(ref)) != 0
967 		 && GC_REF_GET_COLOR(ref) == GC_BLACK) {
968 			GC_TRACE_REF(ref, "removing from buffer");
969 			GC_REMOVE_FROM_BUFFER(ref);
970 		} else if (GC_TYPE(ref) == IS_REFERENCE) {
971 			if (Z_REFCOUNTED(((zend_reference*)ref)->val)) {
972 				ref = Z_COUNTED(((zend_reference*)ref)->val);
973 				goto tail_call;
974 			}
975 			return;
976 		} else {
977 			return;
978 		}
979 
980 		if (GC_TYPE(ref) == IS_OBJECT) {
981 			zend_object_get_gc_t get_gc;
982 			zend_object *obj = (zend_object*)ref;
983 
984 			if (EXPECTED(!(GC_FLAGS(ref) & IS_OBJ_FREE_CALLED) &&
985 		                 (get_gc = obj->handlers->get_gc) != NULL)) {
986 				int n;
987 				zval *zv, *end;
988 				zval tmp;
989 
990 				ZVAL_OBJ(&tmp, obj);
991 				ht = get_gc(&tmp, &zv, &n);
992 				end = zv + n;
993 				if (EXPECTED(!ht)) {
994 					if (!n) return;
995 					while (!Z_REFCOUNTED_P(--end)) {
996 						if (zv == end) return;
997 					}
998 				}
999 				while (zv != end) {
1000 					if (Z_REFCOUNTED_P(zv)) {
1001 						ref = Z_COUNTED_P(zv);
1002 						gc_remove_nested_data_from_buffer(ref, NULL);
1003 					}
1004 					zv++;
1005 				}
1006 				if (EXPECTED(!ht)) {
1007 					ref = Z_COUNTED_P(zv);
1008 					goto tail_call;
1009 				}
1010 				if (GC_ADDRESS(GC_INFO(ht)) != 0 && GC_REF_GET_COLOR(ht) == GC_BLACK) {
1011 					GC_TRACE_REF(ht, "removing from buffer");
1012 					GC_REMOVE_FROM_BUFFER(ht);
1013 				}
1014 			} else {
1015 				return;
1016 			}
1017 		} else if (GC_TYPE(ref) == IS_ARRAY) {
1018 			ht = (zend_array*)ref;
1019 		} else {
1020 			return;
1021 		}
1022 
1023 		if (!ht->nNumUsed) return;
1024 		p = ht->arData;
1025 		end = p + ht->nNumUsed;
1026 		while (1) {
1027 			end--;
1028 			zv = &end->val;
1029 			if (Z_TYPE_P(zv) == IS_INDIRECT) {
1030 				zv = Z_INDIRECT_P(zv);
1031 			}
1032 			if (Z_REFCOUNTED_P(zv)) {
1033 				break;
1034 			}
1035 			if (p == end) return;
1036 		}
1037 		while (p != end) {
1038 			zv = &p->val;
1039 			if (Z_TYPE_P(zv) == IS_INDIRECT) {
1040 				zv = Z_INDIRECT_P(zv);
1041 			}
1042 			if (Z_REFCOUNTED_P(zv)) {
1043 				ref = Z_COUNTED_P(zv);
1044 				gc_remove_nested_data_from_buffer(ref, NULL);
1045 			}
1046 			p++;
1047 		}
1048 		zv = &p->val;
1049 		if (Z_TYPE_P(zv) == IS_INDIRECT) {
1050 			zv = Z_INDIRECT_P(zv);
1051 		}
1052 		ref = Z_COUNTED_P(zv);
1053 		goto tail_call;
1054 	} while (0);
1055 }
1056 
1057 ZEND_API int zend_gc_collect_cycles(void)
1058 {
1059 	int count = 0;
1060 
1061 	if (GC_G(roots).next != &GC_G(roots)) {
1062 		gc_root_buffer *current, *next, *orig_next_to_free;
1063 		zend_refcounted *p;
1064 		gc_root_buffer to_free;
1065 		uint32_t gc_flags = 0;
1066 		gc_additional_buffer *additional_buffer_snapshot;
1067 #if ZEND_GC_DEBUG
1068 		zend_bool orig_gc_full;
1069 #endif
1070 
1071 		if (GC_G(gc_active)) {
1072 			return 0;
1073 		}
1074 
1075 		GC_TRACE("Collecting cycles");
1076 		GC_G(gc_runs)++;
1077 		GC_G(gc_active) = 1;
1078 
1079 		GC_TRACE("Marking roots");
1080 		gc_mark_roots();
1081 		GC_TRACE("Scanning roots");
1082 		gc_scan_roots();
1083 
1084 #if ZEND_GC_DEBUG
1085 		orig_gc_full = GC_G(gc_full);
1086 		GC_G(gc_full) = 0;
1087 #endif
1088 
1089 		GC_TRACE("Collecting roots");
1090 		additional_buffer_snapshot = GC_G(additional_buffer);
1091 		count = gc_collect_roots(&gc_flags);
1092 #if ZEND_GC_DEBUG
1093 		GC_G(gc_full) = orig_gc_full;
1094 #endif
1095 		GC_G(gc_active) = 0;
1096 
1097 		if (GC_G(to_free).next == &GC_G(to_free)) {
1098 			/* nothing to free */
1099 			GC_TRACE("Nothing to free");
1100 			return 0;
1101 		}
1102 
1103 		/* Copy global to_free list into local list */
1104 		to_free.next = GC_G(to_free).next;
1105 		to_free.prev = GC_G(to_free).prev;
1106 		to_free.next->prev = &to_free;
1107 		to_free.prev->next = &to_free;
1108 
1109 		/* Free global list */
1110 		GC_G(to_free).next = &GC_G(to_free);
1111 		GC_G(to_free).prev = &GC_G(to_free);
1112 
1113 		orig_next_to_free = GC_G(next_to_free);
1114 
1115 #if ZEND_GC_DEBUG
1116 		orig_gc_full = GC_G(gc_full);
1117 		GC_G(gc_full) = 0;
1118 #endif
1119 
1120 		if (gc_flags & GC_HAS_DESTRUCTORS) {
1121 			GC_TRACE("Calling destructors");
1122 
1123 			/* Remember reference counters before calling destructors */
1124 			current = to_free.next;
1125 			while (current != &to_free) {
1126 				current->refcount = GC_REFCOUNT(current->ref);
1127 				current = current->next;
1128 			}
1129 
1130 			/* Call destructors */
1131 			current = to_free.next;
1132 			while (current != &to_free) {
1133 				p = current->ref;
1134 				GC_G(next_to_free) = current->next;
1135 				if (GC_TYPE(p) == IS_OBJECT) {
1136 					zend_object *obj = (zend_object*)p;
1137 
1138 					if (!(GC_FLAGS(obj) & IS_OBJ_DESTRUCTOR_CALLED)) {
1139 						GC_TRACE_REF(obj, "calling destructor");
1140 						GC_FLAGS(obj) |= IS_OBJ_DESTRUCTOR_CALLED;
1141 						if (obj->handlers->dtor_obj
1142 						 && (obj->handlers->dtor_obj != zend_objects_destroy_object
1143 						  || obj->ce->destructor)) {
1144 							GC_REFCOUNT(obj)++;
1145 							obj->handlers->dtor_obj(obj);
1146 							GC_REFCOUNT(obj)--;
1147 						}
1148 					}
1149 				}
1150 				current = GC_G(next_to_free);
1151 			}
1152 
1153 			/* Remove values captured in destructors */
1154 			current = to_free.next;
1155 			while (current != &to_free) {
1156 				GC_G(next_to_free) = current->next;
1157 				if (GC_REFCOUNT(current->ref) > current->refcount) {
1158 					gc_remove_nested_data_from_buffer(current->ref, current);
1159 				}
1160 				current = GC_G(next_to_free);
1161 			}
1162 		}
1163 
1164 		/* Destroy zvals */
1165 		GC_TRACE("Destroying zvals");
1166 		GC_G(gc_active) = 1;
1167 		current = to_free.next;
1168 		while (current != &to_free) {
1169 			p = current->ref;
1170 			GC_G(next_to_free) = current->next;
1171 			GC_TRACE_REF(p, "destroying");
1172 			if (GC_TYPE(p) == IS_OBJECT) {
1173 				zend_object *obj = (zend_object*)p;
1174 
1175 				EG(objects_store).object_buckets[obj->handle] = SET_OBJ_INVALID(obj);
1176 				GC_TYPE(obj) = IS_NULL;
1177 				if (!(GC_FLAGS(obj) & IS_OBJ_FREE_CALLED)) {
1178 					GC_FLAGS(obj) |= IS_OBJ_FREE_CALLED;
1179 					if (obj->handlers->free_obj) {
1180 						GC_REFCOUNT(obj)++;
1181 						obj->handlers->free_obj(obj);
1182 						GC_REFCOUNT(obj)--;
1183 					}
1184 				}
1185 				SET_OBJ_BUCKET_NUMBER(EG(objects_store).object_buckets[obj->handle], EG(objects_store).free_list_head);
1186 				EG(objects_store).free_list_head = obj->handle;
1187 				p = current->ref = (zend_refcounted*)(((char*)obj) - obj->handlers->offset);
1188 			} else if (GC_TYPE(p) == IS_ARRAY) {
1189 				zend_array *arr = (zend_array*)p;
1190 
1191 				GC_TYPE(arr) = IS_NULL;
1192 
1193 				/* GC may destroy arrays with rc>1. This is valid and safe. */
1194 				HT_ALLOW_COW_VIOLATION(arr);
1195 
1196 				zend_hash_destroy(arr);
1197 			}
1198 			current = GC_G(next_to_free);
1199 		}
1200 
1201 		/* Free objects */
1202 		current = to_free.next;
1203 		while (current != &to_free) {
1204 			next = current->next;
1205 			p = current->ref;
1206 			if (EXPECTED(current >= GC_G(buf) && current < GC_G(buf) + GC_ROOT_BUFFER_MAX_ENTRIES)) {
1207 				current->prev = GC_G(unused);
1208 				GC_G(unused) = current;
1209 			}
1210 			efree(p);
1211 			current = next;
1212 		}
1213 
1214 		while (GC_G(additional_buffer) != additional_buffer_snapshot) {
1215 			gc_additional_buffer *next = GC_G(additional_buffer)->next;
1216 			efree(GC_G(additional_buffer));
1217 			GC_G(additional_buffer) = next;
1218 		}
1219 
1220 		GC_TRACE("Collection finished");
1221 		GC_G(collected) += count;
1222 		GC_G(next_to_free) = orig_next_to_free;
1223 #if ZEND_GC_DEBUG
1224 		GC_G(gc_full) = orig_gc_full;
1225 #endif
1226 		GC_G(gc_active) = 0;
1227 	}
1228 
1229 	return count;
1230 }
1231 
1232 /*
1233  * Local variables:
1234  * tab-width: 4
1235  * c-basic-offset: 4
1236  * indent-tabs-mode: t
1237  * End:
1238  * vim600: sw=4 ts=4 fdm=marker
1239  * vim<600: sw=4 ts=4
1240  *
1241  * vim:noexpandtab:
1242  */
1243