From 4e0de1be13723f0a8d75ed7a2be87bbbcca71dd6 Mon Sep 17 00:00:00 2001 From: lazerg Date: Mon, 10 Aug 2026 15:29:25 +0500 Subject: [PATCH 1/2] Add a stack limit check in php_count_recursive --- NEWS | 2 ++ ext/standard/array.c | 13 ++++++++ .../array/count_recursive_stack_limit.phpt | 32 +++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 ext/standard/tests/array/count_recursive_stack_limit.phpt diff --git a/NEWS b/NEWS index 6447fa7bc881..68eb850d62c5 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/standard/array.c b/ext/standard/array.c index bb23c99c5709..6782c1b3083c 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -611,6 +611,13 @@ 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 0; + } +#endif + if (!(GC_FLAGS(ht) & GC_IMMUTABLE)) { if (GC_IS_RECURSIVE(ht)) { php_error_docref(NULL, E_WARNING, "Recursion detected"); @@ -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)); + if (UNEXPECTED(EG(exception))) { + break; + } } } ZEND_HASH_FOREACH_END(); @@ -656,6 +666,9 @@ PHP_FUNCTION(count) cnt = zend_hash_num_elements(Z_ARRVAL_P(array)); } else { cnt = php_count_recursive(Z_ARRVAL_P(array)); + if (UNEXPECTED(EG(exception))) { + RETURN_THROWS(); + } } RETURN_LONG(cnt); break; 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 000000000000..a7f3918ba350 --- /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-- + +--INI-- +zend.max_allowed_stack_size=256K +--FILE-- +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 From a11b59fee3faac7b4e9d078c58c8d6f4021c9f70 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Tue, 11 Aug 2026 18:34:27 +0500 Subject: [PATCH 2/2] Signal stack overflow through the return value of php_count_recursive --- ext/spl/spl_observer.c | 6 +++++- ext/standard/array.c | 13 ++++++++----- ext/standard/php_array.h | 1 + 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/ext/spl/spl_observer.c b/ext/spl/spl_observer.c index 56cdbdd4b5f3..9a9710e069f2 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 6782c1b3083c..85a017eff7f9 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -614,14 +614,15 @@ PHPAPI zend_long php_count_recursive(HashTable *ht) /* {{{ */ #ifdef ZEND_CHECK_STACK_LIMIT if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) { zend_call_stack_size_error(); - return 0; + 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); } @@ -630,10 +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)); - if (UNEXPECTED(EG(exception))) { + 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(); @@ -666,7 +669,7 @@ PHP_FUNCTION(count) cnt = zend_hash_num_elements(Z_ARRVAL_P(array)); } else { cnt = php_count_recursive(Z_ARRVAL_P(array)); - if (UNEXPECTED(EG(exception))) { + if (UNEXPECTED(cnt < 0)) { RETURN_THROWS(); } } diff --git a/ext/standard/php_array.h b/ext/standard/php_array.h index 2a35af603808..24e320a5313c 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);