Commit 71a12445070 for php.net

commit 71a124450705e790901838af74157eb086a414a5
Author: Lazizbek Ergashev <lazerg2@gmail.com>
Date:   Mon Aug 10 16:10:59 2026 +0500

    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

diff --git a/NEWS b/NEWS
index d7dadb551bc..e3995c5664b 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 41123d43bcc..bb23c99c570 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 00000000000..7854583f9ec
--- /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