[php-src] PHP-8.5: Fix use-after-free in RecursiveIteratorIterator on reentry

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

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

Fix use-after-free in RecursiveIteratorIterator on reentry

move_forward_ex() caches the active sub-iterator, then calls the inner
iterator's move_forward(), which can re-enter userland. A next() that
rewinds or advances the RecursiveIteratorIterator frees that sub-iterator,
and the following validity check then reads freed memory. Re-fetch the
sub-iterator after the call, the same way the no-more-elements branch
already re-checks the level after endChildren().

Closes GH-22466

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


Diff:

diff --git a/ext/spl/spl_iterators.c b/ext/spl/spl_iterators.c
index db100d228341..eb068a50c91f 100644
--- a/ext/spl/spl_iterators.c
+++ b/ext/spl/spl_iterators.c
@@ -273,6 +273,7 @@ static void spl_recursive_it_move_forward_ex(spl_recursive_it_object *object, zv
 						zend_clear_exception();
 					}
 				}
+				iterator = object->iterators[object->level].iterator;
 				ZEND_FALLTHROUGH;
 			case RS_START:
 				if (iterator->funcs->valid(iterator) == FAILURE) {
diff --git a/ext/spl/tests/recursiveiteratoriterator_rewind_during_next.phpt b/ext/spl/tests/recursiveiteratoriterator_rewind_during_next.phpt
new file mode 100644
index 000000000000..3c9863e81a30
--- /dev/null
+++ b/ext/spl/tests/recursiveiteratoriterator_rewind_during_next.phpt
@@ -0,0 +1,41 @@
+--TEST--
+RecursiveIteratorIterator: rewind() re-entered from an inner next() must not use-after-free
+--FILE--
+<?php
+class Reenter 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++;
+        if ($this->rii && $this->depth === 1 && $this->pos === 1 && !self::$fired) {
+            self::$fired = true;
+            $this->rii->rewind();
+        }
+    }
+    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 {
+        $c = new Reenter($this->current(), $this->depth + 1);
+        $c->rii = $this->rii;
+        return $c;
+    }
+}
+$root = new Reenter([[10, 11], [20, 21]]);
+$rii = new RecursiveIteratorIterator($root, RecursiveIteratorIterator::SELF_FIRST);
+$root->rii = $rii;
+$seen = [];
+foreach ($rii as $v) {
+    if (is_array($v)) { $v = '[' . implode(',', $v) . ']'; }
+    $seen[] = $v;
+    if (count($seen) > 20) { $seen[] = '...'; break; }
+}
+echo implode(' ', $seen), "\n";
+echo "done\n";
+?>
+--EXPECT--
+[10,11] 10 [10,11] 10 11 [20,21] 20 21
+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.