Skip to content

Fix use-after-free in RecursiveIteratorIterator on reentry#22466

Open
iliaal wants to merge 1 commit into
php:PHP-8.4from
iliaal:fix/spl-recursiveiterator-reentry-uaf
Open

Fix use-after-free in RecursiveIteratorIterator on reentry#22466
iliaal wants to merge 1 commit into
php:PHP-8.4from
iliaal:fix/spl-recursiveiterator-reentry-uaf

Conversation

@iliaal

@iliaal iliaal commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

A RecursiveIterator whose next() calls rewind() (or next()) on the RecursiveIteratorIterator wrapping it triggers a use-after-free: the move-forward loop caches the current sub-iterator, the inner move_forward() re-enters userland, the reentrant rewind() frees that sub-iterator, and the following validity check then reads freed memory. Re-fetching the sub-iterator after the inner call closes the window, mirroring the level re-check the no-more-elements branch already does after endChildren().

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().
Comment thread ext/spl/spl_iterators.c
zend_clear_exception();
}
}
iterator = object->iterators[object->level].iterator;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since endChildren()/valid() are also reentry points, it might be worth dtoring object->iterators[object->level].iterator at line 431 with a new test wdyt ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and it's a separate pre-existing UAF (reproduces on base without this hunk). endChildren() re-entering via $this->next() frees the level's sub-iterator and pops the level inside the reentrant call, then the outer frame's zend_iterator_dtor(iterator) at line 432 frees the stale local again. rewind() reentry is already safe: it unwinds to level 0 and the if (object->level > 0) re-check catches it.

So it needs more than reloading object->iterators[object->level].iterator there: the reentrant next() already tore the level down, so the outer frame has to notice and skip the dtor/decrement. I'll fix it in a follow-up with its own test rather than widen this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants