From 96eb611a2979624fb3ea15b84e3538dd6466a7bf Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 26 Jun 2026 08:15:04 -0400 Subject: [PATCH] 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(). --- ext/spl/spl_iterators.c | 1 + ...veiteratoriterator_rewind_during_next.phpt | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 ext/spl/tests/recursiveiteratoriterator_rewind_during_next.phpt 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-- +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