Commit fa8a9d1ca99 for php
commit fa8a9d1ca996c6954a4220e8c3b4e84234bbc2ad
Author: Eyüp Can Akman <eyupcanakman@gmail.com>
Date: Thu Nov 12 16:11:38 2026 +0800
Fix stack overflow in filter_var() and filter_var_array() (#25349)
Deeply nested arrays could overflow the C stack while being filtered.
Check the stack limit before descending, and stop processing array elements and
keys once an exception is pending. This prevents repeated stack errors from a
second deep entry.
Closes #25349
diff --git a/NEWS b/NEWS
index 18b784ca064..e8051495b75 100644
--- a/NEWS
+++ b/NEWS
@@ -45,6 +45,10 @@ PHP NEWS
. Fixed the argument blamed by the imagepolygon(), imageopenpolygon() and
imagefilledpolygon() too few points error. (lacatoire)
+- Filter:
+ . Fixed stack overflow in filter_var() and filter_var_array() with deeply
+ nested arrays. (Eyüp Can Akman)
+
- MBString:
. Fixed stack overflow in mb_check_encoding() and mb_convert_encoding() with
deeply nested arrays. (Eyüp Can Akman)
diff --git a/ext/filter/filter.c b/ext/filter/filter.c
index 7e1a0311099..a98fc56df24 100644
--- a/ext/filter/filter.c
+++ b/ext/filter/filter.c
@@ -393,6 +393,13 @@ static void php_zval_filter_recursive(zval *value, zend_long filter, zend_long f
if (Z_TYPE_P(value) == IS_ARRAY) {
zval *element;
+#ifdef ZEND_CHECK_STACK_LIMIT
+ if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) {
+ zend_call_stack_size_error();
+ return;
+ }
+#endif
+
if (Z_IS_RECURSIVE_P(value)) {
return;
}
@@ -403,6 +410,10 @@ static void php_zval_filter_recursive(zval *value, zend_long filter, zend_long f
if (Z_TYPE_P(element) == IS_ARRAY) {
SEPARATE_ARRAY(element);
php_zval_filter_recursive(element, filter, flags, options, charset, copy);
+ if (EG(exception)) {
+ Z_UNPROTECT_RECURSION_P(value);
+ return;
+ }
} else {
php_zval_filter(element, filter, flags, options, charset, copy);
}
@@ -590,6 +601,10 @@ static void php_filter_array_handler(zval *input, HashTable *op_ht, zend_long op
Z_TYPE_P(arg_elm) == IS_ARRAY ? 0 : zval_get_long(arg_elm),
0, FILTER_REQUIRE_SCALAR
);
+ if (EG(exception)) {
+ zval_ptr_dtor(&nval);
+ RETURN_THROWS();
+ }
zend_hash_update(Z_ARRVAL_P(return_value), arg_key, &nval);
}
} ZEND_HASH_FOREACH_END();
diff --git a/ext/filter/tests/filter_recursive_stack_limit.phpt b/ext/filter/tests/filter_recursive_stack_limit.phpt
new file mode 100644
index 00000000000..fb8d4c6f385
--- /dev/null
+++ b/ext/filter/tests/filter_recursive_stack_limit.phpt
@@ -0,0 +1,47 @@
+--TEST--
+Stack overflow in filter_var() and filter_var_array() with deeply nested arrays
+--EXTENSIONS--
+filter
+--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
+$deep = 'x';
+for ($i = 0; $i < 30000; $i++) {
+ $deep = [$deep];
+}
+
+/* Two top-level entries: the second must not be visited once the
+ * stack limit error has been thrown, so only one Error is thrown. */
+try {
+ filter_var([$deep, $deep], FILTER_DEFAULT, FILTER_REQUIRE_ARRAY);
+} catch (Error $e) {
+ echo $e->getMessage(), "\n";
+ var_dump($e->getPrevious());
+}
+try {
+ filter_var_array(['a' => $deep, 'b' => $deep], ['a' => ['flags' => FILTER_REQUIRE_ARRAY], 'b' => ['flags' => FILTER_REQUIRE_ARRAY]]);
+} catch (Error $e) {
+ echo $e->getMessage(), "\n";
+ var_dump($e->getPrevious());
+}
+
+unset($e);
+while (is_array($deep)) {
+ $deep = $deep[0];
+}
+?>
+--EXPECTF--
+Maximum call stack size of %d bytes (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion?
+NULL
+Maximum call stack size of %d bytes (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion?
+NULL