Commit 6f6c9e35e83 for php.net
commit 6f6c9e35e83a1f12b4ff86472c6af9fc66d74eda
Author: Ilija Tovilo <ilija.tovilo@me.com>
Date: Fri Jan 9 16:00:28 2026 +0100
Fix infinite loop in GC destructor fiber
zend_object_release(&fiber->std) may restart the fiber due to finally. Any
thrown exception must be remembered and unset so that the next fiber may
successfully start.
Fixes OSS-Fuzz #471533782
Closes GH-20884
diff --git a/NEWS b/NEWS
index d54ff4f6039..8201bf8cf9c 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,7 @@ PHP NEWS
- Core:
. Fixed bug GH-20837 (NULL dereference when calling ob_start() in shutdown
function triggered by bailout in php_output_lock_error()). (timwolla)
+ . Fix OSS-Fuzz #471533782 (Infinite loop in GC destructor fiber). (ilutov)
- MbString:
. Fixed bug GH-20833 (mb_str_pad() divide by zero if padding string is
diff --git a/Zend/tests/fibers/oss-fuzz-471533782-001.phpt b/Zend/tests/fibers/oss-fuzz-471533782-001.phpt
new file mode 100644
index 00000000000..755f18449b0
--- /dev/null
+++ b/Zend/tests/fibers/oss-fuzz-471533782-001.phpt
@@ -0,0 +1,33 @@
+--TEST--
+OSS-Fuzz #471533782: Infinite loop in GC destructor fiber
+--FILE--
+<?php
+
+class Cycle {
+ public $self;
+ public function __construct() {
+ $this->self = $this;
+ }
+ public function __destruct() {
+ try {
+ Fiber::suspend();
+ } finally {
+ throw new Exception();
+ }
+ }
+}
+
+$f = new Fiber(function () {
+ new Cycle();
+ gc_collect_cycles();
+});
+$f->start();
+
+?>
+--EXPECTF--
+Fatal error: Uncaught Exception in %s:%d
+Stack trace:
+#0 [internal function]: Cycle->__destruct()
+#1 [internal function]: gc_destructor_fiber()
+#2 {main}
+ thrown in %s on line %d
diff --git a/Zend/tests/fibers/oss-fuzz-471533782-002.phpt b/Zend/tests/fibers/oss-fuzz-471533782-002.phpt
new file mode 100644
index 00000000000..3dbac0aac75
--- /dev/null
+++ b/Zend/tests/fibers/oss-fuzz-471533782-002.phpt
@@ -0,0 +1,34 @@
+--TEST--
+OSS-Fuzz #471533782: Infinite loop in GC destructor fiber
+--FILE--
+<?php
+
+class Cycle {
+ public $self;
+ public function __construct() {
+ $this->self = $this;
+ }
+ public function __destruct() {
+ try {
+ Fiber::suspend();
+ } finally {
+ Fiber::suspend();
+ }
+ }
+}
+
+$f = new Fiber(function () {
+ new Cycle();
+ gc_collect_cycles();
+});
+$f->start();
+
+?>
+--EXPECTF--
+Fatal error: Uncaught FiberError: Cannot suspend in a force-closed fiber in %s:%d
+Stack trace:
+#0 %s(%d): Fiber::suspend()
+#1 [internal function]: Cycle->__destruct()
+#2 [internal function]: gc_destructor_fiber()
+#3 {main}
+ thrown in %s on line %d
diff --git a/Zend/zend_gc.c b/Zend/zend_gc.c
index f73fef50e16..7b2ff49a736 100644
--- a/Zend/zend_gc.c
+++ b/Zend/zend_gc.c
@@ -76,6 +76,7 @@
#include "zend_types.h"
#include "zend_weakrefs.h"
#include "zend_string.h"
+#include "zend_exceptions.h"
#ifndef GC_BENCH
# define GC_BENCH 0
@@ -1872,6 +1873,17 @@ static zend_fiber *gc_create_destructor_fiber(void)
return fiber;
}
+static void remember_prev_exception(zend_object **prev_exception)
+{
+ if (EG(exception)) {
+ if (*prev_exception) {
+ zend_exception_set_previous(EG(exception), *prev_exception);
+ }
+ *prev_exception = EG(exception);
+ EG(exception) = NULL;
+ }
+}
+
static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end)
{
ZEND_ASSERT(!GC_G(dtor_fiber_running));
@@ -1887,6 +1899,9 @@ static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end)
zend_fiber_resume(fiber, NULL, NULL);
}
+ zend_object *exception = NULL;
+ remember_prev_exception(&exception);
+
for (;;) {
/* At this point, fiber has executed until suspension */
GC_TRACE("resumed from destructor fiber");
@@ -1900,7 +1915,9 @@ static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end)
/* We do not own the fiber anymore. It may be collected if the
* application does not reference it. */
zend_object_release(&fiber->std);
+ remember_prev_exception(&exception);
fiber = gc_create_destructor_fiber();
+ remember_prev_exception(&exception);
continue;
} else {
/* Fiber suspended itself after calling all destructors */
@@ -1908,6 +1925,8 @@ static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end)
break;
}
}
+
+ EG(exception) = exception;
}
ZEND_API int zend_gc_collect_cycles(void)