From e0c17b3020a7c3dcbda1fdf763de5d457c202da3 Mon Sep 17 00:00:00 2001 From: lazerg Date: Thu, 6 Aug 2026 21:18:56 +0500 Subject: [PATCH 1/2] Fix GH-23088: Stack overflow when comparing deeply nested arrays --- NEWS | 4 ++++ Zend/tests/gh23088.phpt | 40 ++++++++++++++++++++++++++++++++++++++++ Zend/zend_operators.c | 19 +++++++++++++++++-- 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 Zend/tests/gh23088.phpt diff --git a/NEWS b/NEWS index 474db936ef15..8e6c57cb761a 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,10 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.4.25 +- Core: + . Fixed bug GH-23088 (Stack overflow when comparing deeply nested arrays). + (Lazizbek Ergashev) + - Date: . Fixed leak on double DatePeriod::__construct() call. (ilutov) diff --git a/Zend/tests/gh23088.phpt b/Zend/tests/gh23088.phpt new file mode 100644 index 000000000000..17bcb091d018 --- /dev/null +++ b/Zend/tests/gh23088.phpt @@ -0,0 +1,40 @@ +--TEST-- +GH-23088 (Stack overflow when comparing deeply nested arrays) +--SKIPIF-- + +--INI-- +zend.max_allowed_stack_size=256K +--FILE-- +getMessage(), PHP_EOL; +} + +try { + var_dump($a === $b); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +Maximum call stack size reached during array comparison +Maximum call stack size reached during array comparison diff --git a/Zend/zend_operators.c b/Zend/zend_operators.c index 0a7c335326e5..a0548b124847 100644 --- a/Zend/zend_operators.c +++ b/Zend/zend_operators.c @@ -2417,8 +2417,16 @@ ZEND_API bool ZEND_FASTCALL zend_is_identical(const zval *op1, const zval *op2) case IS_STRING: return zend_string_equals(Z_STR_P(op1), Z_STR_P(op2)); case IS_ARRAY: - return (Z_ARRVAL_P(op1) == Z_ARRVAL_P(op2) || - zend_hash_compare(Z_ARRVAL_P(op1), Z_ARRVAL_P(op2), (compare_func_t) hash_zval_identical_function, 1) == 0); + if (Z_ARRVAL_P(op1) == Z_ARRVAL_P(op2)) { + return 1; + } +#ifdef ZEND_CHECK_STACK_LIMIT + if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) { + zend_throw_error(NULL, "Maximum call stack size reached during array comparison"); + return 0; + } +#endif + return zend_hash_compare(Z_ARRVAL_P(op1), Z_ARRVAL_P(op2), (compare_func_t) hash_zval_identical_function, 1) == 0; case IS_OBJECT: return (Z_OBJ_P(op1) == Z_OBJ_P(op2)); default: @@ -3423,6 +3431,13 @@ ZEND_API int ZEND_FASTCALL zend_compare_symbol_tables(HashTable *ht1, HashTable ZEND_API int ZEND_FASTCALL zend_compare_arrays(zval *a1, zval *a2) /* {{{ */ { +#ifdef ZEND_CHECK_STACK_LIMIT + if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) { + zend_throw_error(NULL, "Maximum call stack size reached during array comparison"); + return ZEND_UNCOMPARABLE; + } +#endif + return zend_compare_symbol_tables(Z_ARRVAL_P(a1), Z_ARRVAL_P(a2)); } /* }}} */ From 7fdba9d5a9e4b24e60dbefb6955f136cdb102bdb Mon Sep 17 00:00:00 2001 From: lazerg Date: Fri, 7 Aug 2026 19:21:09 +0500 Subject: [PATCH 2/2] Move array comparison stack check into zend_hash_compare() --- Zend/tests/gh18572.phpt | 2 +- Zend/tests/gh23088.phpt | 4 ++-- Zend/zend_hash.c | 7 +++++++ Zend/zend_operators.c | 19 ++----------------- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/Zend/tests/gh18572.phpt b/Zend/tests/gh18572.phpt index ff178ebef24f..cf45d2afaaba 100644 --- a/Zend/tests/gh18572.phpt +++ b/Zend/tests/gh18572.phpt @@ -36,4 +36,4 @@ try { } ?> --EXPECTREGEX-- -(Maximum call stack size reached during object comparison|Nesting level too deep - recursive dependency\?) +(Maximum call stack size reached during (object )?comparison|Nesting level too deep - recursive dependency\?) diff --git a/Zend/tests/gh23088.phpt b/Zend/tests/gh23088.phpt index 17bcb091d018..59153a1f2ba3 100644 --- a/Zend/tests/gh23088.phpt +++ b/Zend/tests/gh23088.phpt @@ -36,5 +36,5 @@ try { ?> --EXPECT-- -Maximum call stack size reached during array comparison -Maximum call stack size reached during array comparison +Maximum call stack size reached during comparison +Maximum call stack size reached during comparison diff --git a/Zend/zend_hash.c b/Zend/zend_hash.c index 23637b94bceb..82d0318428fa 100644 --- a/Zend/zend_hash.c +++ b/Zend/zend_hash.c @@ -3214,6 +3214,13 @@ ZEND_API int zend_hash_compare(HashTable *ht1, HashTable *ht2, compare_func_t co return 0; } +#ifdef ZEND_CHECK_STACK_LIMIT + if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) { + zend_throw_error(NULL, "Maximum call stack size reached during comparison"); + return ZEND_UNCOMPARABLE; + } +#endif + /* It's enough to protect only one of the arrays. * The second one may be referenced from the first and this may cause * false recursion detection. diff --git a/Zend/zend_operators.c b/Zend/zend_operators.c index a0548b124847..0a7c335326e5 100644 --- a/Zend/zend_operators.c +++ b/Zend/zend_operators.c @@ -2417,16 +2417,8 @@ ZEND_API bool ZEND_FASTCALL zend_is_identical(const zval *op1, const zval *op2) case IS_STRING: return zend_string_equals(Z_STR_P(op1), Z_STR_P(op2)); case IS_ARRAY: - if (Z_ARRVAL_P(op1) == Z_ARRVAL_P(op2)) { - return 1; - } -#ifdef ZEND_CHECK_STACK_LIMIT - if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) { - zend_throw_error(NULL, "Maximum call stack size reached during array comparison"); - return 0; - } -#endif - return zend_hash_compare(Z_ARRVAL_P(op1), Z_ARRVAL_P(op2), (compare_func_t) hash_zval_identical_function, 1) == 0; + return (Z_ARRVAL_P(op1) == Z_ARRVAL_P(op2) || + zend_hash_compare(Z_ARRVAL_P(op1), Z_ARRVAL_P(op2), (compare_func_t) hash_zval_identical_function, 1) == 0); case IS_OBJECT: return (Z_OBJ_P(op1) == Z_OBJ_P(op2)); default: @@ -3431,13 +3423,6 @@ ZEND_API int ZEND_FASTCALL zend_compare_symbol_tables(HashTable *ht1, HashTable ZEND_API int ZEND_FASTCALL zend_compare_arrays(zval *a1, zval *a2) /* {{{ */ { -#ifdef ZEND_CHECK_STACK_LIMIT - if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) { - zend_throw_error(NULL, "Maximum call stack size reached during array comparison"); - return ZEND_UNCOMPARABLE; - } -#endif - return zend_compare_symbol_tables(Z_ARRVAL_P(a1), Z_ARRVAL_P(a2)); } /* }}} */