From 50e493b7ee1c46fe27ac81ff0d164475aed9c60f Mon Sep 17 00:00:00 2001 From: PurHur Date: Mon, 8 Jun 2026 14:20:24 +0000 Subject: [PATCH] VM: invoke __destruct during gc_collect_cycles on cyclic graphs (#6519) Zend defers destructor invocation on cyclic object graphs until gc_collect_cycles() runs. Call invokeUserDestructor before releasing unreachable objects in CycleCollector (phpc_gc.c already deleted). Co-authored-by: Cursor --- lib/VM/CycleCollector.php | 1 + lib/VM/ObjectLifetime.php | 9 +++++ .../runtime/gc_collect_cycles_destruct.phpt | 19 ++++++++++ test/unit/GcCollectCyclesDestructTest.php | 36 +++++++++++++++++++ 4 files changed, 65 insertions(+) create mode 100644 test/compliance/cases/runtime/gc_collect_cycles_destruct.phpt create mode 100644 test/unit/GcCollectCyclesDestructTest.php diff --git a/lib/VM/CycleCollector.php b/lib/VM/CycleCollector.php index 3c8d556a178..2ee3b05a9ce 100644 --- a/lib/VM/CycleCollector.php +++ b/lib/VM/CycleCollector.php @@ -55,6 +55,7 @@ public static function collect(Context $ctx): int if (isset($marked[$object->id])) { continue; } + ObjectLifetime::invokeDestructorBeforeGcRelease($object); ObjectRegistry::release($object); ++$collected; } diff --git a/lib/VM/ObjectLifetime.php b/lib/VM/ObjectLifetime.php index 73abd0fabcb..b35bb910fd2 100644 --- a/lib/VM/ObjectLifetime.php +++ b/lib/VM/ObjectLifetime.php @@ -87,6 +87,15 @@ public static function releaseDirectObject(Variable $var): void * * @see https://github.com/php/php-src/blob/master/Zend/zend_objects.c zend_objects_destroy_object */ + /** Invoke __destruct before gc_collect_cycles() frees an unreachable object (#6519, zend_gc.c). */ + public static function invokeDestructorBeforeGcRelease(ObjectEntry $object): void + { + $vm = self::$vm; + if (null !== $vm && !$object->destructorInvoked) { + $vm->invokeUserDestructor($object); + } + } + public static function invokeUnsetDestructor(VM $vm, Variable $var): void { $var = $var->resolveIndirect(); diff --git a/test/compliance/cases/runtime/gc_collect_cycles_destruct.phpt b/test/compliance/cases/runtime/gc_collect_cycles_destruct.phpt new file mode 100644 index 00000000000..1b00365c274 --- /dev/null +++ b/test/compliance/cases/runtime/gc_collect_cycles_destruct.phpt @@ -0,0 +1,19 @@ +--TEST-- +runtime gc_collect_cycles() — cyclic objects invoke __destruct (#6519) +--FILE-- +next = $b; +$b->next = $a; +unset($a, $b); +gc_collect_cycles(); +--EXPECT-- +dtor +dtor diff --git a/test/unit/GcCollectCyclesDestructTest.php b/test/unit/GcCollectCyclesDestructTest.php new file mode 100644 index 00000000000..df37f998e16 --- /dev/null +++ b/test/unit/GcCollectCyclesDestructTest.php @@ -0,0 +1,36 @@ +next = $b; +$b->next = $a; +unset($a, $b); +gc_collect_cycles(); +PHP; + $rt = new Runtime(); + $block = $rt->parseAndCompile($code, 'gc_cycle_destruct.php'); + ob_start(); + $rt->run($block); + $this->assertSame("dtor\ndtor\n", ob_get_clean()); + } +}