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)