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

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-10T13:10:59+02:00

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

Add a stack limit check in php_compact_var() (#23126)

php_compact_var() recurses once per nesting level with no stack check, so passing a deeply nested array to compact() exhausts the native stack and the process dies with a segfault.

This adds the same stack limit check ext/standard already uses in var.c and http.c, so the call throws an Error instead of crashing.

Fixes GH-23115

Changed paths:
  A  ext/standard/tests/array/gh23115.phpt
  M  NEWS
  M  ext/standard/array.c


Diff:

diff --git a/NEWS b/NEWS
index d7dadb551bcb..e3995c5664bd 100644
--- a/NEWS
+++ b/NEWS
@@ -74,6 +74,8 @@ PHP                                                                        NEWS
     nested arrays). (Lazizbek Ergashev)
   . Fixed bug GH-23113 (Stack overflow in array_replace_recursive() with deeply
     nested arrays). (Lazizbek Ergashev)
+  . Fixed bug GH-23115 (Stack overflow in compact() with 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 41123d43bcc6..bb23c99c5709 100644
--- a/ext/standard/array.c
+++ b/ext/standard/array.c
@@ -2699,7 +2699,7 @@ PHP_FUNCTION(extract)
 }
 /* }}} */
 
-static void php_compact_var(HashTable *eg_active_symbol_table, zval *return_value, zval *entry, uint32_t pos) /* {{{ */
+static zend_result php_compact_var(HashTable *eg_active_symbol_table, zval *return_value, zval *entry, uint32_t pos) /* {{{ */
 {
 	zval *value_ptr, data;
 
@@ -2717,25 +2717,43 @@ static void php_compact_var(HashTable *eg_active_symbol_table, zval *return_valu
 			}
 		} else {
 			php_error_docref_unchecked(NULL, E_WARNING, "Undefined variable $%S", Z_STR_P(entry));
+			/* A user error handler may have thrown. */
+			return EG(exception) ? FAILURE : SUCCESS;
 		}
 	} else if (Z_TYPE_P(entry) == IS_ARRAY) {
+		zend_result result = SUCCESS;
+
+#ifdef ZEND_CHECK_STACK_LIMIT
+		if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) {
+			zend_call_stack_size_error();
+			return FAILURE;
+		}
+#endif
 		if (Z_REFCOUNTED_P(entry)) {
 			if (Z_IS_RECURSIVE_P(entry)) {
 				zend_throw_error(NULL, "Recursion detected");
-				return;
+				return FAILURE;
 			}
 			Z_PROTECT_RECURSION_P(entry);
 		}
 		ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(entry), value_ptr) {
-			php_compact_var(eg_active_symbol_table, return_value, value_ptr, pos);
+			if (UNEXPECTED(php_compact_var(eg_active_symbol_table, return_value, value_ptr, pos) == FAILURE)) {
+				result = FAILURE;
+				break;
+			}
 		} ZEND_HASH_FOREACH_END();
 		if (Z_REFCOUNTED_P(entry)) {
 			Z_UNPROTECT_RECURSION_P(entry);
 		}
+
+		return result;
 	} else {
 		php_error_docref(NULL, E_WARNING, "Argument #%d must be string or array of strings, %s given", pos, zend_zval_value_name(entry));
-		return;
+		/* A user error handler may have thrown. */
+		return EG(exception) ? FAILURE : SUCCESS;
 	}
+
+	return SUCCESS;
 }
 /* }}} */
 
@@ -2767,7 +2785,9 @@ PHP_FUNCTION(compact)
 	}
 
 	for (i = 0; i < num_args; i++) {
-		php_compact_var(symbol_table, return_value, &args[i], i + 1);
+		if (UNEXPECTED(php_compact_var(symbol_table, return_value, &args[i], i + 1) == FAILURE)) {
+			RETURN_THROWS();
+		}
 	}
 }
 /* }}} */
diff --git a/ext/standard/tests/array/gh23115.phpt b/ext/standard/tests/array/gh23115.phpt
new file mode 100644
index 000000000000..7854583f9ec6
--- /dev/null
+++ b/ext/standard/tests/array/gh23115.phpt
@@ -0,0 +1,41 @@
+--TEST--
+GH-23115 (Stack overflow in compact with 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. */
+$names = [];
+for ($i = 0; $i < 30000; $i++) {
+    $names = [$names, []];
+}
+
+try {
+    compact($names);
+} catch (Throwable $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+    var_dump($e->getPrevious());
+}
+
+try {
+    compact($names, $names);
+} 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
+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.