[php-src] PHP-8.4: Add a stack limit check in php_count_recursive() (#23197)

Lazizbek Ergashev via GitHub <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Lazizbek Ergashev (lazerg)
Committer: GitHub (web-flow)
Pusher: arnaud-lb
Date: 2026-08-11T19:01:11+02:00

Commit: https://github.com/php/php-src/commit/c6d19577eba066fedaf846e26f45483e0ec2fbec
Raw diff: https://github.com/php/php-src/commit/c6d19577eba066fedaf846e26f45483e0ec2fbec.diff

Add a stack limit check in php_count_recursive() (#23197)

Changed paths:
  A  ext/standard/tests/array/count_recursive_stack_limit.phpt
  M  NEWS
  M  ext/spl/spl_observer.c
  M  ext/standard/array.c
  M  ext/standard/php_array.h


Diff:

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/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 bb23c99c5709..85a017eff7f9 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 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);
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--
+<?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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.