Skip to content

Fix stack overflow in count() with COUNT_RECURSIVE and deep arrays - #23197

Merged
arnaud-lb merged 2 commits into
php:PHP-8.4from
lazerg:fix/count-recursive-stack-limit
Aug 11, 2026
Merged

Fix stack overflow in count() with COUNT_RECURSIVE and deep arrays#23197
arnaud-lb merged 2 commits into
php:PHP-8.4from
lazerg:fix/count-recursive-stack-limit

Conversation

@lazerg

@lazerg lazerg commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

php_count_recursive() recurses once per nesting level with no stack check, so count($array, COUNT_RECURSIVE) on a deeply nested array exhausts the native stack and the process dies with a segfault.

This is the separate PR offered in #23125, applying the same stack limit check the neighbouring functions in ext/standard use, so the call throws an Error instead of crashing. There is no GitHub issue for it.

The loop over the elements also breaks once the error is thrown, otherwise every remaining sibling recurses again and chains another Error onto the first one, and count() returns through RETURN_THROWS() in that case.

Comment thread ext/standard/array.c Outdated
Comment on lines +633 to +634
cnt += php_count_recursive(Z_ARRVAL_P(element));
if (UNEXPECTED(EG(exception))) {

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.

Similarly to compact(), we can avoid loading EG(exception) after each element by signaling errors via the return value of php_count_recursive() (we should return -1 after zend_call_stack_size_error())

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.

Done. php_count_recursive() returns -1 after zend_call_stack_size_error(), the loop branches on the returned value, and PHP_FUNCTION(count) checks cnt < 0 instead of EG(exception).

One spot needed the same treatment as in compact(): the "Recursion detected" warning. A user error handler can throw there, and until now the caller's EG(exception) check is what stopped the iteration. That path returns EG(exception) ? -1 : 0, so the load stays on the cold warning path and out of the loop. A plain warning with no handler still returns 0 and counting continues, unchanged.

I also updated the other caller, SplObjectStorage::count(COUNT_RECURSIVE), so it throws instead of returning -1, and documented the sentinel in php_array.h since the function is exported.

ext/standard, ext/spl and Zend are green here, same as before. I re-checked that the test still fails without the new early exit: with the sub_cnt < 0 break removed, getPrevious() is an Error again.

Comment thread ext/standard/array.c Outdated
@@ -624,6 +631,9 @@ PHPAPI zend_long php_count_recursive(HashTable *ht) /* {{{ */
ZVAL_DEREF(element);
if (Z_TYPE_P(element) == IS_ARRAY) {
cnt += php_count_recursive(Z_ARRVAL_P(element));

@staabm staabm Aug 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I recently improved performance in php userland (PHPStan) by removing recursion and use iteration instead.
would it be an option to turn this code into iterative logic - which I guess likely means no stack overflow checks would be necessary?
(assuming iteration is faster than recursion in C land)

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.

It is possible, but it is a much bigger change than this fix needs. Each nesting level also sets a GC recursion flag that has to be cleared on the way out, so an iterative version needs its own explicit stack of hash positions and flagged tables, in a function that every count($a, COUNT_RECURSIVE) call goes through.

This PR is one of a series of stack limit fixes (#23090, #23111, #23113, #23115, #23125, #23126). They all use the same guard, so I would rather keep this one consistent with them and small enough to review. The check itself is one comparison against EG(stack_limit), so performance is not really the motivation here.

Worth looking at as a separate PR though, if a maintainer is interested.

@arnaud-lb arnaud-lb Aug 11, 2026

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.

Stack limit checks have the best value/effort ratio, so we should prioritize this over eliminating recursion (at least when hitting the limit is very unlikely). Eliminating recursion is ideal of course, but it may not necessarily be faster.

@lazerg
lazerg force-pushed the fix/count-recursive-stack-limit branch from fd87339 to a11b59f Compare August 11, 2026 13:50
@arnaud-lb
arnaud-lb merged commit c6d1957 into php:PHP-8.4 Aug 11, 2026
18 checks passed
arnaud-lb added a commit that referenced this pull request Aug 11, 2026
* PHP-8.5:
  Add a stack limit check in php_count_recursive() (#23197)
@arnaud-lb

Copy link
Copy Markdown
Member

Thank you!

pull Bot pushed a commit to wudi/php-src that referenced this pull request Aug 11, 2026
* PHP-8.4:
  Add a stack limit check in php_count_recursive() (php#23197)
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.

3 participants