From 54421223c67dc8f0c46f25bcde8b6efda23a20e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ey=C3=BCp=20Can=20Akman?= Date: Thu, 5 Nov 2026 20:54:09 +0300 Subject: [PATCH] Fix stack overflow in filter_var() and filter_var_array() php_zval_filter_recursive() goes one C call deeper per nesting level without checking the stack limit, so deep enough input segfaults. Check the stack limit there, and stop the element and key loops once an exception is pending so a second deep entry does not chain another Error. --- NEWS | 4 ++ ext/filter/filter.c | 15 ++++++ .../tests/filter_recursive_stack_limit.phpt | 47 +++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 ext/filter/tests/filter_recursive_stack_limit.phpt diff --git a/NEWS b/NEWS index 9ae8c7add120..d6bb6323ffff 100644 --- a/NEWS +++ b/NEWS @@ -24,6 +24,10 @@ PHP NEWS . Fixed bug GH-25268 (NULL php_libxml_ref_obj dereference in dom_objects_store_clone_obj when cloning Dom\Notation). (David Carlier) +- 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 7e1a03110994..a98fc56df240 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 000000000000..fb8d4c6f3854 --- /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-- + +--INI-- +zend.max_allowed_stack_size=256K +--FILE-- +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