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