[php-src] PHP-8.4: Fix use-after-free in RecursiveIteratorIterator on reentrant teardown

Ilia Alshanetsky <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-06-27T15:14:03-04:00

Commit: https://github.com/php/php-src/commit/efbf47300b1c1f561b2c76b92ddf27bb987a6fbc
Raw diff: https://github.com/php/php-src/commit/efbf47300b1c1f561b2c76b92ddf27bb987a6fbc.diff

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

Changed paths:
  A  ext/spl/tests/recursiveiteratoriterator_next_during_endchildren.phpt
  A  ext/spl/tests/recursiveiteratoriterator_next_during_valid.phpt
  M  ext/spl/spl_iterators.c


Diff:

diff --git a/ext/spl/spl_iterators.c b/ext/spl/spl_iterators.c
index eb068a50c91f..425bd99f37c0 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 000000000000..b3c80b2232a8
--- /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 000000000000..a2dcf4695e36
--- /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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.