Commit c6d19577eba for php.net
commit c6d19577eba066fedaf846e26f45483e0ec2fbec
Author: Lazizbek Ergashev <lazerg2@gmail.com>
Date: Tue Aug 11 22:01:11 2026 +0500
Add a stack limit check in php_count_recursive() (#23197)
diff --git a/NEWS b/NEWS
index 6447fa7bc88..68eb850d62c 100644
--- a/NEWS
+++ b/NEWS
@@ -78,6 +78,8 @@ PHP NEWS
nested arrays). (Lazizbek Ergashev)
. Fixed bug GH-23115 (Stack overflow in compact() with deeply nested
arrays). (Lazizbek Ergashev)
+ . Fixed stack overflow in count() with COUNT_RECURSIVE and deeply nested
+ arrays. (Lazizbek Ergashev)
- Streams:
. Fixed bug GH-15836 (Use-after-free when a user stream filter accesses
diff --git a/ext/spl/spl_observer.c b/ext/spl/spl_observer.c
index 56cdbdd4b5f..9a9710e069f 100644
--- a/ext/spl/spl_observer.c
+++ b/ext/spl/spl_observer.c
@@ -679,7 +679,11 @@ PHP_METHOD(SplObjectStorage, count)
}
if (mode == PHP_COUNT_RECURSIVE) {
- RETURN_LONG(php_count_recursive(&intern->storage));
+ zend_long count = php_count_recursive(&intern->storage);
+ if (UNEXPECTED(count < 0)) {
+ RETURN_THROWS();
+ }
+ RETURN_LONG(count);
}
RETURN_LONG(zend_hash_num_elements(&intern->storage));
diff --git a/ext/standard/array.c b/ext/standard/array.c
index bb23c99c570..85a017eff7f 100644
--- a/ext/standard/array.c
+++ b/ext/standard/array.c
@@ -611,10 +611,18 @@ PHPAPI zend_long php_count_recursive(HashTable *ht) /* {{{ */
zend_long cnt = 0;
zval *element;
+#ifdef ZEND_CHECK_STACK_LIMIT
+ if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) {
+ zend_call_stack_size_error();
+ return -1;
+ }
+#endif
+
if (!(GC_FLAGS(ht) & GC_IMMUTABLE)) {
if (GC_IS_RECURSIVE(ht)) {
php_error_docref(NULL, E_WARNING, "Recursion detected");
- return 0;
+ /* A user error handler may have thrown. */
+ return EG(exception) ? -1 : 0;
}
GC_PROTECT_RECURSION(ht);
}
@@ -623,7 +631,12 @@ PHPAPI zend_long php_count_recursive(HashTable *ht) /* {{{ */
ZEND_HASH_FOREACH_VAL(ht, element) {
ZVAL_DEREF(element);
if (Z_TYPE_P(element) == IS_ARRAY) {
- cnt += php_count_recursive(Z_ARRVAL_P(element));
+ zend_long sub_cnt = php_count_recursive(Z_ARRVAL_P(element));
+ if (UNEXPECTED(sub_cnt < 0)) {
+ cnt = -1;
+ break;
+ }
+ cnt += sub_cnt;
}
} ZEND_HASH_FOREACH_END();
@@ -656,6 +669,9 @@ PHP_FUNCTION(count)
cnt = zend_hash_num_elements(Z_ARRVAL_P(array));
} else {
cnt = php_count_recursive(Z_ARRVAL_P(array));
+ if (UNEXPECTED(cnt < 0)) {
+ RETURN_THROWS();
+ }
}
RETURN_LONG(cnt);
break;
diff --git a/ext/standard/php_array.h b/ext/standard/php_array.h
index 2a35af60380..24e320a5313 100644
--- a/ext/standard/php_array.h
+++ b/ext/standard/php_array.h
@@ -29,6 +29,7 @@ PHPAPI int php_array_merge(HashTable *dest, HashTable *src);
PHPAPI int php_array_merge_recursive(HashTable *dest, HashTable *src);
PHPAPI int php_array_replace_recursive(HashTable *dest, HashTable *src);
PHPAPI int php_multisort_compare(const void *a, const void *b);
+/* Returns -1 and throws if the array is nested too deeply. */
PHPAPI zend_long php_count_recursive(HashTable *ht);
PHPAPI bool php_array_data_shuffle(php_random_algo_with_state engine, zval *array);
diff --git a/ext/standard/tests/array/count_recursive_stack_limit.phpt b/ext/standard/tests/array/count_recursive_stack_limit.phpt
new file mode 100644
index 00000000000..a7f3918ba35
--- /dev/null
+++ b/ext/standard/tests/array/count_recursive_stack_limit.phpt
@@ -0,0 +1,32 @@
+--TEST--
+Stack overflow in count() with COUNT_RECURSIVE and deeply nested arrays
+--SKIPIF--
+<?php
+if (ini_get('zend.max_allowed_stack_size') === false) {
+ die('skip No stack limit support');
+}
+if (getenv('SKIP_ASAN')) {
+ die('skip ASAN needs different stack limit setting due to more stack space usage');
+}
+?>
+--INI--
+zend.max_allowed_stack_size=256K
+--FILE--
+<?php
+/* Two elements per nesting level: the sibling must not be visited once the
+ * stack limit error has been thrown, so only one Error is thrown. */
+$a = [];
+for ($i = 0; $i < 30000; $i++) {
+ $a = [$a, []];
+}
+
+try {
+ count($a, COUNT_RECURSIVE);
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+ var_dump($e->getPrevious());
+}
+?>
+--EXPECTF--
+Error: Maximum call stack size of %d bytes (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion?
+NULL