Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Zend/tests/gc/gc_045.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
Expand Down
107 changes: 107 additions & 0 deletions Zend/tests/gc/gc_051.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
--TEST--
GC 051: Acyclic objects are not added to GC buffer
--FILE--
<?php

function test($x) {
assert($x); // Prevent inlining
}

enum E {
case A;
case B;
case C;
}

#[AllowDynamicProperties]
class AcyclicC {
public int $a;
public string $b;
}

class CyclicC {
public array $a;
}

echo "Enums\n";
test(E::A);
test(E::B);
test(E::C);
var_dump(gc_status()['roots']);

echo "Acyclic object\n";
$o = new AcyclicC;
test($o);
var_dump(gc_status()['roots']);
unset($o);

echo "Cyclic object\n";
$o = new CyclicC;
test($o);
var_dump(gc_status()['roots']);
unset($o);

echo "Acyclic object with dynamic properties\n";
$o = new AcyclicC;
$o->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)
3 changes: 3 additions & 0 deletions Zend/tests/weakrefs/gh10043-008.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
}
Expand Down
47 changes: 47 additions & 0 deletions Zend/zend_API.c
Original file line number Diff line number Diff line change
Expand Up @@ -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_DEL_FLAGS(object, GC_NOT_COLLECTABLE);
prop = zend_hash_update(zend_std_get_properties_ex(object), key, prop);
zval_add_ref(prop);
}
Expand All @@ -1779,6 +1780,7 @@ ZEND_API void object_properties_load(zend_object *object, const HashTable *prope
ZSTR_VAL(object->ce->name), h);
}

GC_DEL_FLAGS(object, GC_NOT_COLLECTABLE);
prop = zend_hash_index_update(zend_std_get_properties_ex(object), h, prop);
zval_add_ref(prop);
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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. */
Comment on lines +2441 to +2442
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also mark classes with custom get_properties handler as potentially cyclic, as this is called by zend_std_get_gc.

Otherwise this looks right to me, as objects with std get_gc and get_properties can not expose anything other than standard properties to the GC.

Lazy objects need to take care of updating GC_NOT_COLLECTABLE: Remove GC_NOT_COLLECTABLE in zend_object_make_lazy() when the initializer is cyclic or may have a ref to the object itself, and add it again in zend_lazy_object_init().

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;
}
/* }}} */
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions Zend/zend_closures.c
Original file line number Diff line number Diff line change
Expand Up @@ -862,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) /* {{{ */
Expand Down
7 changes: 6 additions & 1 deletion Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down
5 changes: 3 additions & 2 deletions Zend/zend_compile.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 | X | | */
/* | | | */
/* Function Flags (unused: 30) | | | */
/* ============== | | | */
Expand Down
Loading
Loading