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