Commit 1cbd3b9ba90 for php.net
commit 1cbd3b9ba90c7296c10bae76a7cd7287ddc9ada3
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Fri Jun 26 08:15:04 2026 -0400
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
diff --git a/ext/spl/spl_iterators.c b/ext/spl/spl_iterators.c
index db100d22834..eb068a50c91 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 00000000000..3c9863e81a3
--- /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