Commit efbf47300b1 for php.net

commit efbf47300b1c1f561b2c76b92ddf27bb987a6fbc
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Fri Jun 26 15:15:47 2026 -0400

    Fix use-after-free in RecursiveIteratorIterator on reentrant teardown

    spl_recursive_it_move_forward_ex() tears down the exhausted level after
    running its sub-iterator, but endChildren() and a sub-iterator's valid()
    can re-enter through $this->next() and tear that level down first. The
    no-more-elements branch then dtored a stale iterator pointer, and valid()
    kept running on a sub-iterator the reentrant call had already freed.
    Guard the teardown on the level's iterator being unchanged, and hold a
    reference on the sub-iterator across valid().

    Closes GH-22478

diff --git a/ext/spl/spl_iterators.c b/ext/spl/spl_iterators.c
index eb068a50c91..425bd99f37c 100644
--- a/ext/spl/spl_iterators.c
+++ b/ext/spl/spl_iterators.c
@@ -257,6 +257,10 @@ static void spl_recursive_it_move_forward_ex(spl_recursive_it_object *object, zv
 	zend_class_entry          *ce;
 	zval                      retval, child;
 	zend_object_iterator      *sub_iter;
+	zend_object               *sub_object;
+	uint32_t                  prev_level;
+	zend_result               valid_result;
+	bool                      reentered;

 	SPL_FETCH_SUB_ITERATOR(iterator, object);

@@ -276,7 +280,17 @@ static void spl_recursive_it_move_forward_ex(spl_recursive_it_object *object, zv
 				iterator = object->iterators[object->level].iterator;
 				ZEND_FALLTHROUGH;
 			case RS_START:
-				if (iterator->funcs->valid(iterator) == FAILURE) {
+				sub_object = Z_OBJ(object->iterators[object->level].zobject);
+				prev_level = object->level;
+				GC_ADDREF(sub_object);
+				valid_result = iterator->funcs->valid(iterator);
+				reentered = object->level != prev_level
+					|| object->iterators[object->level].iterator != iterator;
+				OBJ_RELEASE(sub_object);
+				if (reentered) {
+					return;
+				}
+				if (valid_result == FAILURE) {
 					break;
 				}
 				object->iterators[object->level].state = RS_TEST;
@@ -424,7 +438,7 @@ static void spl_recursive_it_move_forward_ex(spl_recursive_it_object *object, zv
 					}
 				}
 			}
-			if (object->level > 0) {
+			if (object->level > 0 && object->iterators[object->level].iterator == iterator) {
 				zval garbage;
 				ZVAL_COPY_VALUE(&garbage, &object->iterators[object->level].zobject);
 				ZVAL_UNDEF(&object->iterators[object->level].zobject);
diff --git a/ext/spl/tests/recursiveiteratoriterator_next_during_endchildren.phpt b/ext/spl/tests/recursiveiteratoriterator_next_during_endchildren.phpt
new file mode 100644
index 00000000000..b3c80b2232a
--- /dev/null
+++ b/ext/spl/tests/recursiveiteratoriterator_next_during_endchildren.phpt
@@ -0,0 +1,42 @@
+--TEST--
+RecursiveIteratorIterator: next() re-entered from endChildren() must not use-after-free
+--FILE--
+<?php
+class RIt implements RecursiveIterator {
+    public $data;
+    public $pos = 0;
+    function __construct(array $d) { $this->data = $d; }
+    function current(): mixed { return $this->data[$this->pos] ?? null; }
+    function key(): mixed { return $this->pos; }
+    function next(): void { $this->pos++; }
+    function rewind(): void { $this->pos = 0; }
+    function valid(): bool { return $this->pos < count($this->data); }
+    function hasChildren(): bool { return is_array($this->current()); }
+    function getChildren(): RecursiveIterator { return new RIt($this->current()); }
+}
+
+class ReenterRII extends RecursiveIteratorIterator {
+    public static int $fired = 0;
+    function endChildren(): void {
+        if (self::$fired++ === 0) {
+            $this->next();
+        }
+    }
+}
+
+$tree = [[[1, 2], 3], 4];
+$it = new ReenterRII(new RIt($tree), RecursiveIteratorIterator::SELF_FIRST);
+$seen = [];
+foreach ($it as $v) {
+    $seen[] = is_array($v) ? 'array' : $v;
+    if (count($seen) > 20) {
+        $seen[] = '...';
+        break;
+    }
+}
+echo implode(' ', $seen), "\n";
+echo "done\n";
+?>
+--EXPECT--
+array array 1 2 4
+done
diff --git a/ext/spl/tests/recursiveiteratoriterator_next_during_valid.phpt b/ext/spl/tests/recursiveiteratoriterator_next_during_valid.phpt
new file mode 100644
index 00000000000..a2dcf4695e3
--- /dev/null
+++ b/ext/spl/tests/recursiveiteratoriterator_next_during_valid.phpt
@@ -0,0 +1,47 @@
+--TEST--
+RecursiveIteratorIterator: next() re-entered from a sub-iterator valid() must not use-after-free
+--FILE--
+<?php
+class RIt implements RecursiveIterator {
+    public $data;
+    public $pos = 0;
+    public $rii;
+    public $depth;
+    public static bool $fired = false;
+    function __construct(array $d, $depth = 0) { $this->data = $d; $this->depth = $depth; }
+    function current(): mixed { return $this->data[$this->pos] ?? null; }
+    function key(): mixed { return $this->pos; }
+    function next(): void { $this->pos++; }
+    function rewind(): void { $this->pos = 0; }
+    function valid(): bool {
+        if ($this->rii && $this->depth === 2 && $this->pos === count($this->data) && !self::$fired) {
+            self::$fired = true;
+            $this->rii->next();
+        }
+        return $this->pos < count($this->data);
+    }
+    function hasChildren(): bool { return is_array($this->current()); }
+    function getChildren(): RecursiveIterator {
+        $c = new RIt($this->current(), $this->depth + 1);
+        $c->rii = $this->rii;
+        return $c;
+    }
+}
+
+$root = new RIt([[[1, 2], 3], 4]);
+$rii = new RecursiveIteratorIterator($root, RecursiveIteratorIterator::SELF_FIRST);
+$root->rii = $rii;
+$seen = [];
+foreach ($rii as $v) {
+    $seen[] = is_array($v) ? 'array' : $v;
+    if (count($seen) > 20) {
+        $seen[] = '...';
+        break;
+    }
+}
+echo implode(' ', $seen), "\n";
+echo "done\n";
+?>
+--EXPECT--
+array array 1 2 3 4
+done