From f85545895859ec5681e24460b8f31b6b54dc4f67 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Thu, 12 Dec 2024 19:00:41 +0100 Subject: [PATCH 1/9] Implement acyclic object tracking Acyclic objects don't need to be added to the GC. We know an object is acyclic if all properties are typed as acyclic, and the object does not have any dynamic properties. This is possible to track at runtime with minimal overhead. Fixes GH-17127 --- Zend/tests/gc/gc_045.phpt | 6 ++ Zend/tests/weakrefs/gh10043-008.phpt | 3 + Zend/zend_API.c | 47 ++++++++++ Zend/zend_closures.c | 2 + Zend/zend_compile.h | 5 +- Zend/zend_gc.c | 91 ++++++++++--------- Zend/zend_inheritance.c | 4 + Zend/zend_iterators.c | 1 + Zend/zend_object_handlers.c | 2 + Zend/zend_objects.c | 3 + Zend/zend_objects_API.c | 4 +- Zend/zend_types.h | 2 +- ext/reflection/php_reflection.c | 10 ++ ext/reflection/php_reflection.stub.php | 2 + ext/reflection/php_reflection_arginfo.h | 6 +- ext/reflection/php_reflection_decl.h | 8 +- .../tests/ReflectionClass_toString_001.phpt | 9 +- 17 files changed, 151 insertions(+), 54 deletions(-) diff --git a/Zend/tests/gc/gc_045.phpt b/Zend/tests/gc/gc_045.phpt index 1762be5db1ad9..cbb1fc71e79f9 100644 --- a/Zend/tests/gc/gc_045.phpt +++ b/Zend/tests/gc/gc_045.phpt @@ -11,6 +11,9 @@ class GlobalData class Value { + /* Force object to be added to GC, even though it is acyclic. */ + public $dummy; + public function __destruct() { new Bar(); @@ -19,6 +22,9 @@ class Value class Bar { + /* Force object to be added to GC, even though it is acyclic. */ + public $dummy; + public function __construct() { GlobalData::$bar = $this; diff --git a/Zend/tests/weakrefs/gh10043-008.phpt b/Zend/tests/weakrefs/gh10043-008.phpt index f39c2ddbe1c2e..91645b8ff36b0 100644 --- a/Zend/tests/weakrefs/gh10043-008.phpt +++ b/Zend/tests/weakrefs/gh10043-008.phpt @@ -5,6 +5,9 @@ Self-referencing map entry GC - 008 class Canary extends stdClass { + /* Force object to be added to GC, even though it is acyclic. */ + public $dummy; + public function __construct(public string $name) { } diff --git a/Zend/zend_API.c b/Zend/zend_API.c index e72043ead5870..2bbb84f1fd386 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -1767,6 +1767,7 @@ ZEND_API void object_properties_load(zend_object *object, const HashTable *prope ZSTR_VAL(object->ce->name), property_info != ZEND_WRONG_PROPERTY_INFO ? zend_get_unmangled_property_name(key): ""); } + GC_TYPE_INFO(object) &= ~(GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT); prop = zend_hash_update(zend_std_get_properties_ex(object), key, prop); zval_add_ref(prop); } @@ -1779,6 +1780,7 @@ ZEND_API void object_properties_load(zend_object *object, const HashTable *prope ZSTR_VAL(object->ce->name), h); } + GC_TYPE_INFO(object) &= ~(GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT); prop = zend_hash_index_update(zend_std_get_properties_ex(object), h, prop); zval_add_ref(prop); } @@ -2391,6 +2393,8 @@ ZEND_API zend_result zend_startup_module_ex(zend_module_entry *module) /* {{{ */ } module->module_started = 1; + uint32_t prev_class_count = zend_hash_num_elements(CG(class_table)); + /* Check module dependencies */ if (module->deps) { const zend_module_dep *dep = module->deps; @@ -2433,6 +2437,22 @@ ZEND_API zend_result zend_startup_module_ex(zend_module_entry *module) /* {{{ */ } EG(current_module) = NULL; } + + /* Mark classes with custom get_gc handler as potentially cyclic, even if + * their properties don't indicate so. */ + if (prev_class_count != zend_hash_num_elements(CG(class_table))) { + Bucket *p; + ZEND_HASH_MAP_FOREACH_BUCKET_FROM(CG(class_table), p, prev_class_count) { + zend_class_entry *ce = Z_PTR(p->val); + if ((ce->ce_flags & ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES) + || ce->create_object + || ce->default_object_handlers->get_gc != zend_std_get_gc + || ce->default_object_handlers->get_properties != zend_std_get_properties) { + ce->ce_flags2 |= ZEND_ACC2_MAY_BE_CYCLIC; + } + } ZEND_HASH_FOREACH_END(); + } + return SUCCESS; } /* }}} */ @@ -4414,6 +4434,27 @@ static zend_always_inline bool is_persistent_class(const zend_class_entry *ce) { && ce->info.internal.module->type == MODULE_PERSISTENT; } +static bool zend_type_may_be_cyclic(zend_type type) +{ + if (!ZEND_TYPE_IS_SET(type)) { + return true; + } + + if (!ZEND_TYPE_IS_COMPLEX(type)) { + return ZEND_TYPE_PURE_MASK(type) & (MAY_BE_OBJECT|MAY_BE_ARRAY); + } else if (ZEND_TYPE_IS_UNION(type)) { + const zend_type *list_type; + ZEND_TYPE_LIST_FOREACH(ZEND_TYPE_LIST(type), list_type) { + if (zend_type_may_be_cyclic(*list_type)) { + return true; + } + } ZEND_TYPE_LIST_FOREACH_END(); + return false; + } + + return true; +} + ZEND_API zend_property_info *zend_declare_typed_property(zend_class_entry *ce, zend_string *name, zval *property, int access_type, zend_string *doc_comment, zend_type type) /* {{{ */ { zend_property_info *property_info, *property_info_ptr; @@ -4426,6 +4467,12 @@ ZEND_API zend_property_info *zend_declare_typed_property(zend_class_entry *ce, z } } + if (!(access_type & ZEND_ACC_STATIC) + && !(ce->ce_flags2 & ZEND_ACC2_MAY_BE_CYCLIC) + && zend_type_may_be_cyclic(type)) { + ce->ce_flags2 |= ZEND_ACC2_MAY_BE_CYCLIC; + } + if (ce->type == ZEND_INTERNAL_CLASS) { property_info = pemalloc(sizeof(zend_property_info), 1); } else { diff --git a/Zend/zend_closures.c b/Zend/zend_closures.c index cca69985a0dfe..96eed832c42ef 100644 --- a/Zend/zend_closures.c +++ b/Zend/zend_closures.c @@ -733,6 +733,8 @@ void zend_register_closure_ce(void) /* {{{ */ zend_ce_closure = register_class_Closure(); zend_ce_closure->create_object = zend_closure_new; zend_ce_closure->default_object_handlers = &closure_handlers; + /* FIXME: Potentially infer ZEND_ACC2_MAY_BE_CYCLIC during construction of + * closure? static closures not binding by references can't be cyclic. */ memcpy(&closure_handlers, &std_object_handlers, sizeof(zend_object_handlers)); closure_handlers.free_obj = zend_closure_free_storage; diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 587ae485ec821..75278fcdc0fbc 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -341,10 +341,11 @@ typedef struct _zend_oparray_context { /* Class cannot be serialized or unserialized | | | */ #define ZEND_ACC_NOT_SERIALIZABLE (1 << 29) /* X | | | */ /* | | | */ -/* Class Flags 2 (ce_flags2) (unused: 0-31) | | | */ +/* Class Flags 2 (ce_flags2) (unused: 1-31) | | | */ /* ========================= | | | */ /* | | | */ -/* #define ZEND_ACC2_EXAMPLE (1 << 0) X | | | */ +/* Object may be the root of a cycle | | | */ +#define ZEND_ACC2_MAY_BE_CYCLIC (1 << 0) /* X | | | */ /* | | | */ /* Function Flags (unused: 30) | | | */ /* ============== | | | */ diff --git a/Zend/zend_gc.c b/Zend/zend_gc.c index fc0c9fc0634af..24e4140d6276f 100644 --- a/Zend/zend_gc.c +++ b/Zend/zend_gc.c @@ -251,6 +251,9 @@ #define GC_FETCH_NEXT_UNUSED() \ gc_fetch_next_unused() +#define GC_COLLECTABLE(ref) \ + (!(GC_TYPE_INFO(ref) & (GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT))) + ZEND_API int (*gc_collect_cycles)(void); /* The type of a root buffer entry. @@ -864,7 +867,7 @@ static void gc_scan_black(zend_refcounted *ref, gc_stack *stack) zval *entry = (zval*) Z_PTR_P(zv); zval *weakmap = zv+1; ZEND_ASSERT(Z_REFCOUNTED_P(weakmap)); - if (Z_OPT_COLLECTABLE_P(entry)) { + if (Z_OPT_COLLECTABLE_P(entry) && GC_COLLECTABLE(Z_COUNTED_P(entry))) { GC_UNSET_FROM_WEAKMAP_KEY(entry); if (GC_REF_CHECK_COLOR(Z_COUNTED_P(weakmap), GC_GREY)) { /* Weakmap was scanned in gc_mark_roots, we must @@ -901,7 +904,7 @@ static void gc_scan_black(zend_refcounted *ref, gc_stack *stack) ZEND_ASSERT(Z_TYPE_P(zv+1) == IS_PTR); zval *key = zv; zval *entry = (zval*) Z_PTR_P(zv+1); - if (Z_OPT_COLLECTABLE_P(entry)) { + if (Z_OPT_COLLECTABLE_P(entry) && GC_COLLECTABLE(Z_COUNTED_P(entry))) { GC_UNSET_FROM_WEAKMAP(entry); if (GC_REF_CHECK_COLOR(Z_COUNTED_P(key), GC_GREY)) { /* Key was scanned in gc_mark_roots, we must @@ -934,12 +937,12 @@ static void gc_scan_black(zend_refcounted *ref, gc_stack *stack) ht = obj->handlers->get_gc(obj, &table, &len); n = len; zv = table; - if (UNEXPECTED(ht)) { + if (UNEXPECTED(ht) && GC_COLLECTABLE(ht)) { GC_ADDREF(ht); if (!GC_REF_CHECK_COLOR(ht, GC_BLACK)) { GC_REF_SET_BLACK(ht); for (; n != 0; n--) { - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { ref = Z_COUNTED_P(zv); GC_ADDREF(ref); if (!GC_REF_CHECK_COLOR(ref, GC_BLACK)) { @@ -955,14 +958,14 @@ static void gc_scan_black(zend_refcounted *ref, gc_stack *stack) handle_zvals: for (; n != 0; n--) { - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { ref = Z_COUNTED_P(zv); GC_ADDREF(ref); if (!GC_REF_CHECK_COLOR(ref, GC_BLACK)) { GC_REF_SET_BLACK(ref); zv++; while (--n) { - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { zend_refcounted *ref = Z_COUNTED_P(zv); GC_ADDREF(ref); if (!GC_REF_CHECK_COLOR(ref, GC_BLACK)) { @@ -994,7 +997,7 @@ static void gc_scan_black(zend_refcounted *ref, gc_stack *stack) if (Z_TYPE_P(zv) == IS_INDIRECT) { zv = Z_INDIRECT_P(zv); } - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { ref = Z_COUNTED_P(zv); GC_ADDREF(ref); if (!GC_REF_CHECK_COLOR(ref, GC_BLACK)) { @@ -1005,7 +1008,7 @@ static void gc_scan_black(zend_refcounted *ref, gc_stack *stack) if (Z_TYPE_P(zv) == IS_INDIRECT) { zv = Z_INDIRECT_P(zv); } - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { zend_refcounted *ref = Z_COUNTED_P(zv); GC_ADDREF(ref); if (!GC_REF_CHECK_COLOR(ref, GC_BLACK)) { @@ -1021,7 +1024,7 @@ static void gc_scan_black(zend_refcounted *ref, gc_stack *stack) p++; } } else if (GC_TYPE(ref) == IS_REFERENCE) { - if (Z_COLLECTABLE(((zend_reference*)ref)->val)) { + if (Z_COLLECTABLE(((zend_reference*)ref)->val) && GC_COLLECTABLE(Z_COUNTED(((zend_reference*)ref)->val))) { ref = Z_COUNTED(((zend_reference*)ref)->val); GC_ADDREF(ref); if (!GC_REF_CHECK_COLOR(ref, GC_BLACK)) { @@ -1067,7 +1070,7 @@ static void gc_mark_grey(zend_refcounted *ref, gc_stack *stack) zval *entry = (zval*) Z_PTR_P(zv); zval *weakmap = zv+1; ZEND_ASSERT(Z_REFCOUNTED_P(weakmap)); - if (Z_COLLECTABLE_P(entry)) { + if (Z_COLLECTABLE_P(entry) && GC_COLLECTABLE(Z_COUNTED_P(entry))) { GC_SET_FROM_WEAKMAP_KEY(entry); ref = Z_COUNTED_P(entry); /* Only DELREF if the contribution from the weakmap has @@ -1091,7 +1094,7 @@ static void gc_mark_grey(zend_refcounted *ref, gc_stack *stack) for (; n != 0; n--) { ZEND_ASSERT(Z_TYPE_P(zv) == IS_PTR); zval *entry = (zval*) Z_PTR_P(zv); - if (Z_COLLECTABLE_P(entry)) { + if (Z_COLLECTABLE_P(entry) && GC_COLLECTABLE(Z_COUNTED_P(entry))) { GC_SET_FROM_WEAKMAP(entry); ref = Z_COUNTED_P(entry); /* Only DELREF if the contribution from the weakmap key @@ -1112,12 +1115,12 @@ static void gc_mark_grey(zend_refcounted *ref, gc_stack *stack) ht = obj->handlers->get_gc(obj, &table, &len); n = len; zv = table; - if (UNEXPECTED(ht)) { + if (UNEXPECTED(ht) && GC_COLLECTABLE(ht)) { GC_DELREF(ht); if (!GC_REF_CHECK_COLOR(ht, GC_GREY)) { GC_REF_SET_COLOR(ht, GC_GREY); for (; n != 0; n--) { - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { ref = Z_COUNTED_P(zv); GC_DELREF(ref); if (!GC_REF_CHECK_COLOR(ref, GC_GREY)) { @@ -1132,14 +1135,14 @@ static void gc_mark_grey(zend_refcounted *ref, gc_stack *stack) } handle_zvals: for (; n != 0; n--) { - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { ref = Z_COUNTED_P(zv); GC_DELREF(ref); if (!GC_REF_CHECK_COLOR(ref, GC_GREY)) { GC_REF_SET_COLOR(ref, GC_GREY); zv++; while (--n) { - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { zend_refcounted *ref = Z_COUNTED_P(zv); GC_DELREF(ref); if (!GC_REF_CHECK_COLOR(ref, GC_GREY)) { @@ -1171,7 +1174,7 @@ static void gc_mark_grey(zend_refcounted *ref, gc_stack *stack) if (Z_TYPE_P(zv) == IS_INDIRECT) { zv = Z_INDIRECT_P(zv); } - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { ref = Z_COUNTED_P(zv); GC_DELREF(ref); if (!GC_REF_CHECK_COLOR(ref, GC_GREY)) { @@ -1182,7 +1185,7 @@ static void gc_mark_grey(zend_refcounted *ref, gc_stack *stack) if (Z_TYPE_P(zv) == IS_INDIRECT) { zv = Z_INDIRECT_P(zv); } - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { zend_refcounted *ref = Z_COUNTED_P(zv); GC_DELREF(ref); if (!GC_REF_CHECK_COLOR(ref, GC_GREY)) { @@ -1198,7 +1201,7 @@ static void gc_mark_grey(zend_refcounted *ref, gc_stack *stack) p++; } } else if (GC_TYPE(ref) == IS_REFERENCE) { - if (Z_COLLECTABLE(((zend_reference*)ref)->val)) { + if (Z_COLLECTABLE(((zend_reference*)ref)->val) && GC_COLLECTABLE(Z_COUNTED(((zend_reference*)ref)->val))) { ref = Z_COUNTED(((zend_reference*)ref)->val); GC_DELREF(ref); if (!GC_REF_CHECK_COLOR(ref, GC_GREY)) { @@ -1318,7 +1321,7 @@ static void gc_scan(zend_refcounted *ref, gc_stack *stack) for (; n != 0; n--) { ZEND_ASSERT(Z_TYPE_P(zv) == IS_PTR); zval *entry = (zval*) Z_PTR_P(zv); - if (Z_OPT_COLLECTABLE_P(entry)) { + if (Z_OPT_COLLECTABLE_P(entry) && GC_COLLECTABLE(Z_COUNTED_P(entry))) { ref = Z_COUNTED_P(entry); if (GC_REF_CHECK_COLOR(ref, GC_GREY)) { GC_REF_SET_COLOR(ref, GC_WHITE); @@ -1332,12 +1335,12 @@ static void gc_scan(zend_refcounted *ref, gc_stack *stack) ht = obj->handlers->get_gc(obj, &table, &len); n = len; zv = table; - if (UNEXPECTED(ht)) { + if (UNEXPECTED(ht) && GC_COLLECTABLE(ht)) { if (GC_REF_CHECK_COLOR(ht, GC_GREY)) { GC_REF_SET_COLOR(ht, GC_WHITE); GC_STACK_PUSH((zend_refcounted *) ht); for (; n != 0; n--) { - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { ref = Z_COUNTED_P(zv); if (GC_REF_CHECK_COLOR(ref, GC_GREY)) { GC_REF_SET_COLOR(ref, GC_WHITE); @@ -1352,13 +1355,13 @@ static void gc_scan(zend_refcounted *ref, gc_stack *stack) handle_zvals: for (; n != 0; n--) { - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { ref = Z_COUNTED_P(zv); if (GC_REF_CHECK_COLOR(ref, GC_GREY)) { GC_REF_SET_COLOR(ref, GC_WHITE); zv++; while (--n) { - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { zend_refcounted *ref = Z_COUNTED_P(zv); if (GC_REF_CHECK_COLOR(ref, GC_GREY)) { GC_REF_SET_COLOR(ref, GC_WHITE); @@ -1390,7 +1393,7 @@ static void gc_scan(zend_refcounted *ref, gc_stack *stack) if (Z_TYPE_P(zv) == IS_INDIRECT) { zv = Z_INDIRECT_P(zv); } - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { ref = Z_COUNTED_P(zv); if (GC_REF_CHECK_COLOR(ref, GC_GREY)) { GC_REF_SET_COLOR(ref, GC_WHITE); @@ -1400,7 +1403,7 @@ static void gc_scan(zend_refcounted *ref, gc_stack *stack) if (Z_TYPE_P(zv) == IS_INDIRECT) { zv = Z_INDIRECT_P(zv); } - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { zend_refcounted *ref = Z_COUNTED_P(zv); if (GC_REF_CHECK_COLOR(ref, GC_GREY)) { GC_REF_SET_COLOR(ref, GC_WHITE); @@ -1415,7 +1418,7 @@ static void gc_scan(zend_refcounted *ref, gc_stack *stack) p++; } } else if (GC_TYPE(ref) == IS_REFERENCE) { - if (Z_COLLECTABLE(((zend_reference*)ref)->val)) { + if (Z_COLLECTABLE(((zend_reference*)ref)->val) && GC_COLLECTABLE(Z_COUNTED(((zend_reference*)ref)->val))) { ref = Z_COUNTED(((zend_reference*)ref)->val); if (GC_REF_CHECK_COLOR(ref, GC_GREY)) { GC_REF_SET_COLOR(ref, GC_WHITE); @@ -1532,7 +1535,7 @@ static int gc_collect_white(zend_refcounted *ref, uint32_t *flags, gc_stack *sta for (; n != 0; n--) { ZEND_ASSERT(Z_TYPE_P(zv) == IS_PTR); zval *entry = (zval*) Z_PTR_P(zv); - if (Z_COLLECTABLE_P(entry) && GC_FROM_WEAKMAP_KEY(entry)) { + if (Z_COLLECTABLE_P(entry) && GC_FROM_WEAKMAP_KEY(entry) && GC_COLLECTABLE(Z_COUNTED_P(entry)) && GC_FROM_WEAKMAP_KEY(entry)) { GC_UNSET_FROM_WEAKMAP_KEY(entry); GC_UNSET_FROM_WEAKMAP(entry); ref = Z_COUNTED_P(entry); @@ -1553,7 +1556,7 @@ static int gc_collect_white(zend_refcounted *ref, uint32_t *flags, gc_stack *sta for (; n != 0; n--) { ZEND_ASSERT(Z_TYPE_P(zv) == IS_PTR); zval *entry = (zval*) Z_PTR_P(zv); - if (Z_COLLECTABLE_P(entry) && GC_FROM_WEAKMAP(entry)) { + if (Z_COLLECTABLE_P(entry) && GC_COLLECTABLE(Z_COUNTED_P(entry)) && GC_FROM_WEAKMAP(entry)) { GC_UNSET_FROM_WEAKMAP_KEY(entry); GC_UNSET_FROM_WEAKMAP(entry); ref = Z_COUNTED_P(entry); @@ -1571,12 +1574,12 @@ static int gc_collect_white(zend_refcounted *ref, uint32_t *flags, gc_stack *sta ht = obj->handlers->get_gc(obj, &table, &len); n = len; zv = table; - if (UNEXPECTED(ht)) { + if (UNEXPECTED(ht) && GC_COLLECTABLE(ht)) { GC_ADDREF(ht); if (GC_REF_CHECK_COLOR(ht, GC_WHITE)) { GC_REF_SET_BLACK(ht); for (; n != 0; n--) { - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { ref = Z_COUNTED_P(zv); GC_ADDREF(ref); if (GC_REF_CHECK_COLOR(ref, GC_WHITE)) { @@ -1592,14 +1595,14 @@ static int gc_collect_white(zend_refcounted *ref, uint32_t *flags, gc_stack *sta handle_zvals: for (; n != 0; n--) { - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { ref = Z_COUNTED_P(zv); GC_ADDREF(ref); if (GC_REF_CHECK_COLOR(ref, GC_WHITE)) { GC_REF_SET_BLACK(ref); zv++; while (--n) { - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { zend_refcounted *ref = Z_COUNTED_P(zv); GC_ADDREF(ref); if (GC_REF_CHECK_COLOR(ref, GC_WHITE)) { @@ -1635,7 +1638,7 @@ static int gc_collect_white(zend_refcounted *ref, uint32_t *flags, gc_stack *sta if (Z_TYPE_P(zv) == IS_INDIRECT) { zv = Z_INDIRECT_P(zv); } - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { ref = Z_COUNTED_P(zv); GC_ADDREF(ref); if (GC_REF_CHECK_COLOR(ref, GC_WHITE)) { @@ -1646,7 +1649,7 @@ static int gc_collect_white(zend_refcounted *ref, uint32_t *flags, gc_stack *sta if (Z_TYPE_P(zv) == IS_INDIRECT) { zv = Z_INDIRECT_P(zv); } - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { zend_refcounted *ref = Z_COUNTED_P(zv); GC_ADDREF(ref); if (GC_REF_CHECK_COLOR(ref, GC_WHITE)) { @@ -1662,7 +1665,7 @@ static int gc_collect_white(zend_refcounted *ref, uint32_t *flags, gc_stack *sta p++; } } else if (GC_TYPE(ref) == IS_REFERENCE) { - if (Z_COLLECTABLE(((zend_reference*)ref)->val)) { + if (Z_COLLECTABLE(((zend_reference*)ref)->val) && GC_COLLECTABLE(Z_COUNTED(((zend_reference*)ref)->val))) { ref = Z_COUNTED(((zend_reference*)ref)->val); GC_ADDREF(ref); if (GC_REF_CHECK_COLOR(ref, GC_WHITE)) { @@ -1741,7 +1744,7 @@ static int gc_remove_nested_data_from_buffer(zend_refcounted *ref, gc_root_buffe GC_REMOVE_FROM_BUFFER(ref); count++; } else if (GC_TYPE(ref) == IS_REFERENCE) { - if (Z_COLLECTABLE(((zend_reference*)ref)->val)) { + if (Z_COLLECTABLE(((zend_reference*)ref)->val) && GC_COLLECTABLE(Z_COUNTED(((zend_reference*)ref)->val))) { ref = Z_COUNTED(((zend_reference*)ref)->val); goto tail_call; } @@ -1764,7 +1767,7 @@ static int gc_remove_nested_data_from_buffer(zend_refcounted *ref, gc_root_buffe for (; n != 0; n--) { ZEND_ASSERT(Z_TYPE_P(zv) == IS_PTR); zval *entry = (zval*) Z_PTR_P(zv); - if (Z_OPT_COLLECTABLE_P(entry)) { + if (Z_OPT_COLLECTABLE_P(entry) && GC_COLLECTABLE(Z_COUNTED_P(entry))) { ref = Z_COUNTED_P(entry); GC_STACK_PUSH(ref); } @@ -1775,9 +1778,9 @@ static int gc_remove_nested_data_from_buffer(zend_refcounted *ref, gc_root_buffe ht = obj->handlers->get_gc(obj, &table, &len); n = len; zv = table; - if (UNEXPECTED(ht)) { + if (UNEXPECTED(ht) && GC_COLLECTABLE(ht)) { for (; n != 0; n--) { - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { ref = Z_COUNTED_P(zv); GC_STACK_PUSH(ref); } @@ -1792,11 +1795,11 @@ static int gc_remove_nested_data_from_buffer(zend_refcounted *ref, gc_root_buffe handle_zvals: for (; n != 0; n--) { - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { ref = Z_COUNTED_P(zv); zv++; while (--n) { - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { zend_refcounted *ref = Z_COUNTED_P(zv); GC_STACK_PUSH(ref); } @@ -1823,7 +1826,7 @@ static int gc_remove_nested_data_from_buffer(zend_refcounted *ref, gc_root_buffe if (Z_TYPE_P(zv) == IS_INDIRECT) { zv = Z_INDIRECT_P(zv); } - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { ref = Z_COUNTED_P(zv); p++; while (--n) { @@ -1831,7 +1834,7 @@ static int gc_remove_nested_data_from_buffer(zend_refcounted *ref, gc_root_buffe if (Z_TYPE_P(zv) == IS_INDIRECT) { zv = Z_INDIRECT_P(zv); } - if (Z_COLLECTABLE_P(zv)) { + if (Z_COLLECTABLE_P(zv) && GC_COLLECTABLE(Z_COUNTED_P(zv))) { zend_refcounted *ref = Z_COUNTED_P(zv); GC_STACK_PUSH(ref); } @@ -2285,7 +2288,7 @@ static void zend_gc_remove_root_tmpvars(void) { if (kind == ZEND_LIVE_TMPVAR || kind == ZEND_LIVE_LOOP) { uint32_t var_num = range->var & ~ZEND_LIVE_MASK; zval *var = ZEND_CALL_VAR(ex, var_num); - if (Z_COLLECTABLE_P(var)) { + if (Z_COLLECTABLE_P(var) && GC_COLLECTABLE(Z_COUNTED_P(var))) { GC_REMOVE_FROM_BUFFER(Z_COUNTED_P(var)); } } diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index bac92ccafc4fc..121c3a93b1d5f 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -1880,6 +1880,7 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par ce->parent = parent_ce; ce->default_object_handlers = parent_ce->default_object_handlers; ce->ce_flags |= ZEND_ACC_RESOLVED_PARENT; + ce->ce_flags2 |= (parent_ce->ce_flags2 & ZEND_ACC2_MAY_BE_CYCLIC); /* Inherit properties */ if (parent_ce->default_properties_count) { @@ -2878,6 +2879,9 @@ static void zend_do_traits_property_binding(zend_class_entry *ce, zend_class_ent if (!traits[i]) { continue; } + + ce->ce_flags2 |= (traits[i]->ce_flags2 & ZEND_ACC2_MAY_BE_CYCLIC); + ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(&traits[i]->properties_info, prop_name, property_info) { uint32_t flags = property_info->flags; diff --git a/Zend/zend_iterators.c b/Zend/zend_iterators.c index 64dbb0541a80d..9ee882c4f5445 100644 --- a/Zend/zend_iterators.c +++ b/Zend/zend_iterators.c @@ -59,6 +59,7 @@ ZEND_API void zend_register_iterator_wrapper(void) { INIT_CLASS_ENTRY(zend_iterator_class_entry, "__iterator_wrapper", NULL); zend_iterator_class_entry.default_object_handlers = &iterator_object_handlers; + zend_iterator_class_entry.ce_flags2 |= ZEND_ACC2_MAY_BE_CYCLIC; } static void iter_wrapper_free(zend_object *object) diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index 45eac02949d15..dea4f8d2ae560 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -1257,6 +1257,7 @@ found:; variable_ptr = &EG(error_zval); goto exit; } + GC_TYPE_INFO(zobj) &= ~(GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT); if (UNEXPECTED(!(zobj->ce->ce_flags & ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES))) { if (UNEXPECTED(!zend_deprecated_dynamic_property(zobj, name))) { variable_ptr = &EG(error_zval); @@ -1466,6 +1467,7 @@ ZEND_API zval *zend_std_get_property_ptr_ptr(zend_object *zobj, zend_string *nam return &EG(error_zval); } } + GC_TYPE_INFO(zobj) &= ~(GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT); if (UNEXPECTED(zend_lazy_object_must_init(zobj))) { zobj = zend_lazy_object_init(zobj); if (!zobj) { diff --git a/Zend/zend_objects.c b/Zend/zend_objects.c index 6f6a826389442..526bbf618a532 100644 --- a/Zend/zend_objects.c +++ b/Zend/zend_objects.c @@ -31,6 +31,9 @@ static zend_always_inline void _zend_object_std_init(zend_object *object, zend_c { GC_SET_REFCOUNT(object, 1); GC_TYPE_INFO(object) = GC_OBJECT; + if (!(ce->ce_flags2 & ZEND_ACC2_MAY_BE_CYCLIC)) { + GC_TYPE_INFO(object) |= (GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT); + } object->ce = ce; object->extra_flags = 0; object->handlers = ce->default_object_handlers; diff --git a/Zend/zend_objects_API.c b/Zend/zend_objects_API.c index c19873cf3be30..5f775c944e660 100644 --- a/Zend/zend_objects_API.c +++ b/Zend/zend_objects_API.c @@ -55,7 +55,9 @@ ZEND_API void ZEND_FASTCALL zend_objects_store_call_destructors(zend_objects_sto || obj->ce->destructor) { GC_ADDREF(obj); obj->handlers->dtor_obj(obj); - GC_DELREF(obj); + if (UNEXPECTED(GC_DELREF(obj) == 0)) { + zend_objects_store_del(obj); + } } } } diff --git a/Zend/zend_types.h b/Zend/zend_types.h index 22dbfa9be879b..435addb9e7533 100644 --- a/Zend/zend_types.h +++ b/Zend/zend_types.h @@ -810,7 +810,7 @@ static zend_always_inline uint32_t zval_gc_info(uint32_t gc_type_info) { #define GC_ARRAY IS_ARRAY #define GC_OBJECT IS_OBJECT #define GC_RESOURCE (IS_RESOURCE | (GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT)) -#define GC_REFERENCE (IS_REFERENCE | (GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT)) +#define GC_REFERENCE IS_REFERENCE #define GC_CONSTANT_AST (IS_CONSTANT_AST | (GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT)) /* zval.u1.v.type_flags */ diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 256468e39a444..355254ad7537c 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -4400,6 +4400,16 @@ ZEND_METHOD(ReflectionClass, getAttributes) } /* }}} */ +ZEND_METHOD(ReflectionClass, mayBeCyclic) +{ + reflection_object *intern; + zend_class_entry *ce; + + GET_REFLECTION_OBJECT_PTR(ce); + + RETURN_BOOL(ce->ce_flags2 & ZEND_ACC2_MAY_BE_CYCLIC); +} + /* {{{ Returns the class' constructor if there is one, NULL otherwise */ ZEND_METHOD(ReflectionClass, getConstructor) { diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php index 147e2f18c9e2c..945f9536301bd 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -436,6 +436,8 @@ public function getNamespaceName(): string {} public function getShortName(): string {} public function getAttributes(?string $name = null, int $flags = 0): array {} + + public function mayBeCyclic(): bool {} } class ReflectionObject extends ReflectionClass diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h index 5d45245100185..8f899d89413d9 100644 --- a/ext/reflection/php_reflection_arginfo.h +++ b/ext/reflection/php_reflection_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit php_reflection.stub.php instead. - * Stub hash: dba3ec692c7c90d59d67f6e5323dc31997fc92e0 + * Stub hash: 9b7d8c3c3fc5e1f11d2c17e0df2c368bd93000b7 * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_Reflection_getModifierNames, 0, 1, IS_ARRAY, 0) @@ -367,6 +367,8 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionClass_getAttributes arginfo_class_ReflectionFunctionAbstract_getAttributes +#define arginfo_class_ReflectionClass_mayBeCyclic arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType + ZEND_BEGIN_ARG_INFO_EX(arginfo_class_ReflectionObject___construct, 0, 0, 1) ZEND_ARG_TYPE_INFO(0, object, IS_OBJECT, 0) ZEND_END_ARG_INFO() @@ -852,6 +854,7 @@ ZEND_METHOD(ReflectionClass, inNamespace); ZEND_METHOD(ReflectionClass, getNamespaceName); ZEND_METHOD(ReflectionClass, getShortName); ZEND_METHOD(ReflectionClass, getAttributes); +ZEND_METHOD(ReflectionClass, mayBeCyclic); ZEND_METHOD(ReflectionObject, __construct); ZEND_METHOD(ReflectionProperty, __construct); ZEND_METHOD(ReflectionProperty, __toString); @@ -1146,6 +1149,7 @@ static const zend_function_entry class_ReflectionClass_methods[] = { ZEND_ME(ReflectionClass, getNamespaceName, arginfo_class_ReflectionClass_getNamespaceName, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionClass, getShortName, arginfo_class_ReflectionClass_getShortName, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionClass, getAttributes, arginfo_class_ReflectionClass_getAttributes, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionClass, mayBeCyclic, arginfo_class_ReflectionClass_mayBeCyclic, ZEND_ACC_PUBLIC) ZEND_FE_END }; diff --git a/ext/reflection/php_reflection_decl.h b/ext/reflection/php_reflection_decl.h index 7a458bccd1e5a..745b7528597f7 100644 --- a/ext/reflection/php_reflection_decl.h +++ b/ext/reflection/php_reflection_decl.h @@ -1,12 +1,12 @@ /* This is a generated file, edit php_reflection.stub.php instead. - * Stub hash: dba3ec692c7c90d59d67f6e5323dc31997fc92e0 */ + * Stub hash: 9b7d8c3c3fc5e1f11d2c17e0df2c368bd93000b7 */ -#ifndef ZEND_PHP_REFLECTION_DECL_dba3ec692c7c90d59d67f6e5323dc31997fc92e0_H -#define ZEND_PHP_REFLECTION_DECL_dba3ec692c7c90d59d67f6e5323dc31997fc92e0_H +#ifndef ZEND_PHP_REFLECTION_DECL_9b7d8c3c3fc5e1f11d2c17e0df2c368bd93000b7_H +#define ZEND_PHP_REFLECTION_DECL_9b7d8c3c3fc5e1f11d2c17e0df2c368bd93000b7_H typedef enum zend_enum_PropertyHookType { ZEND_ENUM_PropertyHookType_Get = 1, ZEND_ENUM_PropertyHookType_Set = 2, } zend_enum_PropertyHookType; -#endif /* ZEND_PHP_REFLECTION_DECL_dba3ec692c7c90d59d67f6e5323dc31997fc92e0_H */ +#endif /* ZEND_PHP_REFLECTION_DECL_9b7d8c3c3fc5e1f11d2c17e0df2c368bd93000b7_H */ diff --git a/ext/reflection/tests/ReflectionClass_toString_001.phpt b/ext/reflection/tests/ReflectionClass_toString_001.phpt index fd5d83e917419..67828502b3bdb 100644 --- a/ext/reflection/tests/ReflectionClass_toString_001.phpt +++ b/ext/reflection/tests/ReflectionClass_toString_001.phpt @@ -30,7 +30,7 @@ Class [ class ReflectionClass implements Stringable, Refle Property [ public string $name ] } - - Methods [64] { + - Methods [65] { Method [ private method __clone ] { - Parameters [0] { @@ -514,5 +514,12 @@ Class [ class ReflectionClass implements Stringable, Refle } - Return [ array ] } + + Method [ public method mayBeCyclic ] { + + - Parameters [0] { + } + - Return [ bool ] + } } } From fc13fb6afe79003abec54d4812469ddb654df2bf Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 4 Feb 2026 22:59:39 +0100 Subject: [PATCH 2/9] tmp --- Zend/zend_gc.c | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/Zend/zend_gc.c b/Zend/zend_gc.c index 24e4140d6276f..a69dec48a0813 100644 --- a/Zend/zend_gc.c +++ b/Zend/zend_gc.c @@ -1444,7 +1444,9 @@ static void gc_scan_roots(gc_stack *stack) * make sure to reload pointers. */ idx = GC_FIRST_ROOT; end = GC_G(first_unused); + while (idx != end) { +repeat: current = GC_IDX2PTR(idx); if (GC_IS_ROOT(current->ref)) { if (GC_REF_CHECK_COLOR(current->ref, GC_GREY)) { @@ -1456,15 +1458,9 @@ static void gc_scan_roots(gc_stack *stack) } /* Scan extra roots added during gc_scan */ - while (idx != GC_G(first_unused)) { - current = GC_IDX2PTR(idx); - if (GC_IS_ROOT(current->ref)) { - if (GC_REF_CHECK_COLOR(current->ref, GC_GREY)) { - GC_REF_SET_COLOR(current->ref, GC_WHITE); - gc_scan(current->ref, stack); - } - } - idx++; + if (idx != GC_G(first_unused)) { + end = GC_G(first_unused); + goto repeat; } } @@ -1535,7 +1531,7 @@ static int gc_collect_white(zend_refcounted *ref, uint32_t *flags, gc_stack *sta for (; n != 0; n--) { ZEND_ASSERT(Z_TYPE_P(zv) == IS_PTR); zval *entry = (zval*) Z_PTR_P(zv); - if (Z_COLLECTABLE_P(entry) && GC_FROM_WEAKMAP_KEY(entry) && GC_COLLECTABLE(Z_COUNTED_P(entry)) && GC_FROM_WEAKMAP_KEY(entry)) { + if (Z_COLLECTABLE_P(entry) && GC_FROM_WEAKMAP_KEY(entry) && GC_COLLECTABLE(Z_COUNTED_P(entry))) { GC_UNSET_FROM_WEAKMAP_KEY(entry); GC_UNSET_FROM_WEAKMAP(entry); ref = Z_COUNTED_P(entry); From 1e06e3b9661d946d01a85a94c77c7cf08bbc0d28 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 10 Feb 2026 19:14:49 +0100 Subject: [PATCH 3/9] Fix weakmap --- Zend/zend_gc.c | 1 - Zend/zend_weakrefs.c | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Zend/zend_gc.c b/Zend/zend_gc.c index a69dec48a0813..4ac0fc82710db 100644 --- a/Zend/zend_gc.c +++ b/Zend/zend_gc.c @@ -1444,7 +1444,6 @@ static void gc_scan_roots(gc_stack *stack) * make sure to reload pointers. */ idx = GC_FIRST_ROOT; end = GC_G(first_unused); - while (idx != end) { repeat: current = GC_IDX2PTR(idx); diff --git a/Zend/zend_weakrefs.c b/Zend/zend_weakrefs.c index 8c1263885bf6c..96bfb9486cfec 100644 --- a/Zend/zend_weakrefs.c +++ b/Zend/zend_weakrefs.c @@ -421,6 +421,11 @@ static void zend_weakmap_write_dimension(zend_object *object, zval *offset, zval return; } + if (GC_TYPE_INFO(obj_addr) & (GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT)) { + GC_DEL_FLAGS(obj_addr, GC_NOT_COLLECTABLE); + gc_check_possible_root((zend_refcounted *)obj_addr); + } + zend_weakref_register(obj_addr, ZEND_WEAKREF_ENCODE(&wm->ht, ZEND_WEAKREF_TAG_MAP)); zend_hash_index_add_new(&wm->ht, obj_key, value); } From 4e5fca5efc0f66db58798b6e64d5fa30ebaaa283 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 10 Feb 2026 19:30:24 +0100 Subject: [PATCH 4/9] Fix dynamic property case added in unserializer --- ext/standard/var_unserializer.re | 1 + 1 file changed, 1 insertion(+) diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re index 353c7086d4304..12d85792b932c 100644 --- a/ext/standard/var_unserializer.re +++ b/ext/standard/var_unserializer.re @@ -654,6 +654,7 @@ declared_property: } } + GC_DEL_FLAGS(Z_OBJ_P(rval), GC_NOT_COLLECTABLE); data = zend_hash_add_new(ht, Z_STR(key), &EG(uninitialized_zval)); } else if (ret < 0) { goto failure; From 84b4d70d1537c151b638f6ed43dda62ee81429f7 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 10 Feb 2026 19:36:07 +0100 Subject: [PATCH 5/9] Remove unnecessary gc_check_possible_root() Couldn't find a case where the object won't be released with GC checks, where the object wouldn't already be flagged as collectable. --- Zend/zend_weakrefs.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Zend/zend_weakrefs.c b/Zend/zend_weakrefs.c index 96bfb9486cfec..84675d7e774d1 100644 --- a/Zend/zend_weakrefs.c +++ b/Zend/zend_weakrefs.c @@ -423,7 +423,6 @@ static void zend_weakmap_write_dimension(zend_object *object, zval *offset, zval if (GC_TYPE_INFO(obj_addr) & (GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT)) { GC_DEL_FLAGS(obj_addr, GC_NOT_COLLECTABLE); - gc_check_possible_root((zend_refcounted *)obj_addr); } zend_weakref_register(obj_addr, ZEND_WEAKREF_ENCODE(&wm->ht, ZEND_WEAKREF_TAG_MAP)); From cb8201426b6cbb1bef43c080698a93abb6118413 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 10 Feb 2026 19:39:49 +0100 Subject: [PATCH 6/9] Use GC_{ADD,DEL}_FLAGS() helper macros --- Zend/zend_API.c | 4 ++-- Zend/zend_object_handlers.c | 4 ++-- Zend/zend_objects.c | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 2bbb84f1fd386..aa696d9f60011 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -1767,7 +1767,7 @@ ZEND_API void object_properties_load(zend_object *object, const HashTable *prope ZSTR_VAL(object->ce->name), property_info != ZEND_WRONG_PROPERTY_INFO ? zend_get_unmangled_property_name(key): ""); } - GC_TYPE_INFO(object) &= ~(GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT); + GC_DEL_FLAGS(object, GC_NOT_COLLECTABLE); prop = zend_hash_update(zend_std_get_properties_ex(object), key, prop); zval_add_ref(prop); } @@ -1780,7 +1780,7 @@ ZEND_API void object_properties_load(zend_object *object, const HashTable *prope ZSTR_VAL(object->ce->name), h); } - GC_TYPE_INFO(object) &= ~(GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT); + GC_DEL_FLAGS(object, GC_NOT_COLLECTABLE); prop = zend_hash_index_update(zend_std_get_properties_ex(object), h, prop); zval_add_ref(prop); } diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index dea4f8d2ae560..7ace4d93abf5d 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -1257,7 +1257,7 @@ found:; variable_ptr = &EG(error_zval); goto exit; } - GC_TYPE_INFO(zobj) &= ~(GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT); + GC_DEL_FLAGS(zobj, GC_NOT_COLLECTABLE); if (UNEXPECTED(!(zobj->ce->ce_flags & ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES))) { if (UNEXPECTED(!zend_deprecated_dynamic_property(zobj, name))) { variable_ptr = &EG(error_zval); @@ -1467,7 +1467,7 @@ ZEND_API zval *zend_std_get_property_ptr_ptr(zend_object *zobj, zend_string *nam return &EG(error_zval); } } - GC_TYPE_INFO(zobj) &= ~(GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT); + GC_DEL_FLAGS(zobj, GC_NOT_COLLECTABLE); if (UNEXPECTED(zend_lazy_object_must_init(zobj))) { zobj = zend_lazy_object_init(zobj); if (!zobj) { diff --git a/Zend/zend_objects.c b/Zend/zend_objects.c index 526bbf618a532..960c30d34beba 100644 --- a/Zend/zend_objects.c +++ b/Zend/zend_objects.c @@ -32,7 +32,7 @@ static zend_always_inline void _zend_object_std_init(zend_object *object, zend_c GC_SET_REFCOUNT(object, 1); GC_TYPE_INFO(object) = GC_OBJECT; if (!(ce->ce_flags2 & ZEND_ACC2_MAY_BE_CYCLIC)) { - GC_TYPE_INFO(object) |= (GC_NOT_COLLECTABLE << GC_FLAGS_SHIFT); + GC_ADD_FLAGS(object, GC_NOT_COLLECTABLE); } object->ce = ce; object->extra_flags = 0; From d445bf1ed28afd7fdb13351d5f4036ce4dd82335 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 10 Feb 2026 22:36:12 +0100 Subject: [PATCH 7/9] Fix missing reference unwrap before gc_possible_root() call in JIT --- ext/opcache/jit/zend_jit_ir.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index ace1206682042..845750d7949a5 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -11510,6 +11510,25 @@ static int zend_jit_bind_global(zend_jit_ctx *jit, const zend_op *opline, uint32 ir_END_list(end_inputs); ir_IF_TRUE(if_non_zero); + if (op1_info & (MAY_BE_REF|MAY_BE_GUARD)) { + ir_ref if_ref, ref_ref, if_collectable; + + if_ref = jit_if_Z_TYPE(jit, op1_addr, IS_REFERENCE); + ir_IF_TRUE(if_ref); + + ref_ref = ir_ADD_OFFSET(ref2, offsetof(zend_reference, val)); + + if_collectable = jit_if_COLLECTABLE_ref(jit, ref_ref); + ir_IF_FALSE(if_collectable); + ir_END_list(end_inputs); + ir_IF_TRUE(if_collectable); + + ref_ref = jit_Z_PTR_ref(jit, ref_ref); + + ir_MERGE_WITH_EMPTY_FALSE(if_ref); + ref2 = ir_PHI_2(IR_ADDR, ref_ref, ref2); + } + // JIT: GC_ZVAL_CHECK_POSSIBLE_ROOT(variable_ptr) if_may_not_leak = jit_if_GC_MAY_NOT_LEAK(jit, ref2); ir_IF_TRUE(if_may_not_leak); From e3f4882bd30269b14f7c317c786de5684826eb9c Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Thu, 12 Feb 2026 19:05:12 +0100 Subject: [PATCH 8/9] Also exclude simple stateless closures --- Zend/tests/gc/gc_051.phpt | 105 ++++++++++++++++++++++++++++++++++++++ Zend/zend_closures.c | 6 ++- Zend/zend_compile.c | 7 ++- Zend/zend_compile.h | 2 +- 4 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 Zend/tests/gc/gc_051.phpt diff --git a/Zend/tests/gc/gc_051.phpt b/Zend/tests/gc/gc_051.phpt new file mode 100644 index 0000000000000..c84971d005674 --- /dev/null +++ b/Zend/tests/gc/gc_051.phpt @@ -0,0 +1,105 @@ +--TEST-- +GC 051: Acyclic objects are not added to GC buffer +--FILE-- +dyn = 42; +test($o); +var_dump(gc_status()['roots']); +unset($o); + +echo "Stateless closure\n"; +$o = static function () {}; +test($o); +var_dump(gc_status()['roots']); +unset($o); + +echo "Closure with bindings\n"; +$x = []; +$o = static function () use ($x) {}; +unset($x); +test($o); +var_dump(gc_status()['roots']); +unset($o); + +echo "Closure with static vars\n"; +$o = static function () { + static $x; +}; +test($o); +var_dump(gc_status()['roots']); +unset($o); + +echo "Non-static closure\n"; +$o = function () {}; +test($o); +var_dump(gc_status()['roots']); +unset($o); + +echo "Generator\n"; +$c = static function () { yield; }; // Not collectable +test($c); +$o = $c(); // Collectable +test($o); +var_dump(gc_status()['roots']); +unset($o); +unset($c); + +?> +--EXPECT-- +Enums +int(0) +Acyclic object +int(0) +Cyclic object +int(1) +Acyclic object with dynamic properties +int(1) +Stateless closure +int(0) +Closure with bindings +int(1) +Closure with static vars +int(1) +Non-static closure +int(1) +Generator +int(1) diff --git a/Zend/zend_closures.c b/Zend/zend_closures.c index 96eed832c42ef..d0479428d67ff 100644 --- a/Zend/zend_closures.c +++ b/Zend/zend_closures.c @@ -733,8 +733,6 @@ void zend_register_closure_ce(void) /* {{{ */ zend_ce_closure = register_class_Closure(); zend_ce_closure->create_object = zend_closure_new; zend_ce_closure->default_object_handlers = &closure_handlers; - /* FIXME: Potentially infer ZEND_ACC2_MAY_BE_CYCLIC during construction of - * closure? static closures not binding by references can't be cyclic. */ memcpy(&closure_handlers, &std_object_handlers, sizeof(zend_object_handlers)); closure_handlers.free_obj = zend_closure_free_storage; @@ -864,6 +862,10 @@ ZEND_API void zend_create_closure(zval *res, zend_function *func, zend_class_ent { zend_create_closure_ex(res, func, scope, called_scope, this_ptr, /* is_fake */ (func->common.fn_flags & ZEND_ACC_FAKE_CLOSURE) != 0); + + if (!(func->common.fn_flags2 & ZEND_ACC2_MAY_BE_CYCLIC)) { + GC_ADD_FLAGS(Z_OBJ_P(res), GC_NOT_COLLECTABLE); + } } ZEND_API void zend_create_fake_closure(zval *res, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr) /* {{{ */ diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 23db72bb4fda1..c92c401e348b8 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -5140,7 +5140,7 @@ static zend_result zend_compile_func_array_map(znode *result, zend_ast_list *arg * breaking for the generated call. */ if (callback->kind == ZEND_AST_CALL - && callback->child[0]->kind == ZEND_AST_ZVAL + && callback->child[0]->kind == ZEND_AST_ZVAL && Z_TYPE_P(zend_ast_get_zval(callback->child[0])) == IS_STRING && zend_string_equals_literal_ci(zend_ast_get_str(callback->child[0]), "assert")) { return FAILURE; @@ -8906,6 +8906,11 @@ static zend_op_array *zend_compile_func_decl_ex( zend_do_extended_stmt(NULL); zend_emit_final_return(false); + if ((decl->kind == ZEND_AST_CLOSURE || decl->kind == ZEND_AST_ARROW_FUNC) + && (!(op_array->fn_flags & ZEND_ACC_STATIC) || op_array->static_variables)) { + op_array->fn_flags2 |= ZEND_ACC2_MAY_BE_CYCLIC; + } + pass_two(CG(active_op_array)); zend_oparray_context_end(&orig_oparray_context); diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 75278fcdc0fbc..9f5faa2a7e216 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -345,7 +345,7 @@ typedef struct _zend_oparray_context { /* ========================= | | | */ /* | | | */ /* Object may be the root of a cycle | | | */ -#define ZEND_ACC2_MAY_BE_CYCLIC (1 << 0) /* X | | | */ +#define ZEND_ACC2_MAY_BE_CYCLIC (1 << 0) /* X | X | | */ /* | | | */ /* Function Flags (unused: 30) | | | */ /* ============== | | | */ From aabc161c42efd9500627c5f58e33ef3047e57d06 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Thu, 12 Feb 2026 19:57:32 +0100 Subject: [PATCH 9/9] Fix test for opcache --- Zend/tests/gc/gc_051.phpt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Zend/tests/gc/gc_051.phpt b/Zend/tests/gc/gc_051.phpt index c84971d005674..e4e0880e0ac7f 100644 --- a/Zend/tests/gc/gc_051.phpt +++ b/Zend/tests/gc/gc_051.phpt @@ -3,7 +3,9 @@ GC 051: Acyclic objects are not added to GC buffer --FILE--