From e63e2b7cc8ba7406cb02ec12a0e978579b52b305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20Kocsis?= Date: Mon, 10 Aug 2026 08:40:30 +0200 Subject: [PATCH 1/4] Fix typed reference property writes during Uri\Rfc3986\UriBuilder::reset() (#23144) Originally found in https://github.com/php/php-src/pull/22268#discussion_r3705136377 --- ext/uri/php_uri.c | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/ext/uri/php_uri.c b/ext/uri/php_uri.c index 74a559fd591c..6ede828b69fe 100644 --- a/ext/uri/php_uri.c +++ b/ext/uri/php_uri.c @@ -66,13 +66,14 @@ static zend_always_inline zval *php_uri_deref(zval *zv) return zv; } -#define Z_RFC3986_URI_PROP_SCHEME_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 0)) -#define Z_RFC3986_URI_PROP_USERINFO_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 1)) -#define Z_RFC3986_URI_PROP_HOST_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 2)) -#define Z_RFC3986_URI_PROP_PORT_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 3)) -#define Z_RFC3986_URI_PROP_PATH_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 4)) -#define Z_RFC3986_URI_PROP_QUERY_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 5)) -#define Z_RFC3986_URI_PROP_FRAGMENT_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 6)) +#define Z_RFC3986_URI_PROP_SCHEME_DEREF_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 0)) +#define Z_RFC3986_URI_PROP_USERINFO_DEREF_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 1)) +#define Z_RFC3986_URI_PROP_HOST_DEREF_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 2)) +#define Z_RFC3986_URI_PROP_PORT_DEREF_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 3)) +#define Z_RFC3986_URI_PROP_PATH_P(zv) OBJ_PROP_NUM(Z_OBJ_P(zv), 4) +#define Z_RFC3986_URI_PROP_PATH_DEREF_P(zv) php_uri_deref(Z_RFC3986_URI_PROP_PATH_P(zv)) +#define Z_RFC3986_URI_PROP_QUERY_DEREF_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 5)) +#define Z_RFC3986_URI_PROP_FRAGMENT_DEREF_P(zv) php_uri_deref(OBJ_PROP_NUM(Z_OBJ_P(zv), 6)) static HashTable *uri_get_debug_properties(php_uri_object *object) { @@ -1069,14 +1070,17 @@ PHP_METHOD(Uri_Rfc3986_UriBuilder, reset) { ZEND_PARSE_PARAMETERS_NONE(); - convert_to_null(Z_RFC3986_URI_PROP_SCHEME_P(ZEND_THIS)); - convert_to_null(Z_RFC3986_URI_PROP_USERINFO_P(ZEND_THIS)); - convert_to_null(Z_RFC3986_URI_PROP_HOST_P(ZEND_THIS)); - convert_to_null(Z_RFC3986_URI_PROP_PORT_P(ZEND_THIS)); - zval_ptr_dtor(Z_RFC3986_URI_PROP_PATH_P(ZEND_THIS)); + zend_object *object = Z_OBJ_P(ZEND_THIS); + zval *property = object->properties_table; + const zval *end = property + object->ce->default_properties_count; + + while (property != end) { + zend_object_dtor_property(object, property); + ZVAL_NULL(property); + property++; + } + ZVAL_EMPTY_STRING(Z_RFC3986_URI_PROP_PATH_P(ZEND_THIS)); - convert_to_null(Z_RFC3986_URI_PROP_QUERY_P(ZEND_THIS)); - convert_to_null(Z_RFC3986_URI_PROP_FRAGMENT_P(ZEND_THIS)); RETVAL_COPY(ZEND_THIS); } @@ -1219,13 +1223,13 @@ PHP_METHOD(Uri_Rfc3986_UriBuilder, build) Z_PARAM_OBJECT_OF_CLASS_OR_NULL(base_url, php_uri_ce_rfc3986_uri) ZEND_PARSE_PARAMETERS_END(); - const zval *scheme = Z_RFC3986_URI_PROP_SCHEME_P(ZEND_THIS); - const zval *userinfo = Z_RFC3986_URI_PROP_USERINFO_P(ZEND_THIS); - const zval *host = Z_RFC3986_URI_PROP_HOST_P(ZEND_THIS); - const zval *port = Z_RFC3986_URI_PROP_PORT_P(ZEND_THIS); - const zval *path = Z_RFC3986_URI_PROP_PATH_P(ZEND_THIS); - const zval *query = Z_RFC3986_URI_PROP_QUERY_P(ZEND_THIS); - const zval *fragment = Z_RFC3986_URI_PROP_FRAGMENT_P(ZEND_THIS); + const zval *scheme = Z_RFC3986_URI_PROP_SCHEME_DEREF_P(ZEND_THIS); + const zval *userinfo = Z_RFC3986_URI_PROP_USERINFO_DEREF_P(ZEND_THIS); + const zval *host = Z_RFC3986_URI_PROP_HOST_DEREF_P(ZEND_THIS); + const zval *port = Z_RFC3986_URI_PROP_PORT_DEREF_P(ZEND_THIS); + const zval *path = Z_RFC3986_URI_PROP_PATH_DEREF_P(ZEND_THIS); + const zval *query = Z_RFC3986_URI_PROP_QUERY_DEREF_P(ZEND_THIS); + const zval *fragment = Z_RFC3986_URI_PROP_FRAGMENT_DEREF_P(ZEND_THIS); php_uri_parser_rfc3986_uris *base_uris = NULL; if (base_url != NULL) { From 66ceedae95d9bfc3d286aa86f12ae6c92077f719 Mon Sep 17 00:00:00 2001 From: Weilin Du Date: Mon, 10 Aug 2026 15:58:31 +0800 Subject: [PATCH 2/4] ext/intl: Fix stale intl error state in IntlNumberRangeFormatter (#23191) IntlNumberRangeFormatter::createFromSkeleton() and IntlNumberRangeFormatter::format() did not reset intl error state. This commit fix it. I know this is yet another "error state" fixes. Unfortunately we couldn't use the function macro added before because these are methods, and we can only reset the error state manually so far. I personally hate the error state design. I think we should throw exceptions instead. But considering BC breaks... this is just an idea in the void. IntlNumberRangeFormatter is added in 8.6 so this is the correct branch. --- NEWS | 2 + .../rangeformatter/rangeformatter_class.cpp | 9 +++- .../rangeformatter_create_error_reset.phpt | 54 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 ext/intl/tests/rangeformatter/rangeformatter_create_error_reset.phpt diff --git a/NEWS b/NEWS index b091ea276142..8ea376a2c247 100644 --- a/NEWS +++ b/NEWS @@ -46,6 +46,8 @@ PHP NEWS string. (Weilin Du) . Fixed IntlListFormatter::__construct() leaving stale global error state after successful calls. (Weilin Du) + . Fixed IntlNumberRangeFormatter leaving stale global error state after + successful createFromSkeleton() and format() calls. (Weilin Du) . Implemented GH-20255 (Add a predefined calendar constant in IntlDateFormatter for the proleptic gregorian calendar). (David Carlier) . Added SpoofChecker::areBidiConfusable(). (David Carlier) diff --git a/ext/intl/rangeformatter/rangeformatter_class.cpp b/ext/intl/rangeformatter/rangeformatter_class.cpp index 2dbb60c5b639..95acfccd2452 100644 --- a/ext/intl/rangeformatter/rangeformatter_class.cpp +++ b/ext/intl/rangeformatter/rangeformatter_class.cpp @@ -88,6 +88,8 @@ U_CFUNC PHP_METHOD(IntlNumberRangeFormatter, createFromSkeleton) zend_long collapse; zend_long identityFallback; + intl_error_reset(NULL); + ZEND_PARSE_PARAMETERS_START(4,4) Z_PARAM_STRING(skeleton, skeleton_len) Z_PARAM_STRING(locale, locale_len) @@ -158,7 +160,10 @@ U_CFUNC PHP_METHOD(IntlNumberRangeFormatter, format) zval *start; zval *end; + intl_error_reset(NULL); + IntlNumberRangeFormatter_object* obj = Z_INTL_RANGEFORMATTER_P(ZEND_THIS); + intl_error_reset(RANGEFORMATTER_ERROR_P(obj)); ZEND_PARSE_PARAMETERS_START(2, 2) Z_PARAM_NUMBER(start) @@ -179,13 +184,13 @@ U_CFUNC PHP_METHOD(IntlNumberRangeFormatter, format) INTL_G(error_level) = 0; if (U_FAILURE(error)) { - intl_error_set(NULL, error, "Failed to format number range"); + intl_errors_set(RANGEFORMATTER_ERROR_P(obj), error, "Failed to format number range"); } zend_string *ret = intl_charFromString(result, &error); if (U_FAILURE(error)) { - intl_error_set(NULL, error, "Failed to convert result to UTF-8"); + intl_errors_set(RANGEFORMATTER_ERROR_P(obj), error, "Failed to convert result to UTF-8"); } INTL_G(use_exceptions) = old_use_exception; diff --git a/ext/intl/tests/rangeformatter/rangeformatter_create_error_reset.phpt b/ext/intl/tests/rangeformatter/rangeformatter_create_error_reset.phpt new file mode 100644 index 000000000000..fb19da87d58d --- /dev/null +++ b/ext/intl/tests/rangeformatter/rangeformatter_create_error_reset.phpt @@ -0,0 +1,54 @@ +--TEST-- +IntlNumberRangeFormatter resets stale errors +--EXTENSIONS-- +intl +--SKIPIF-- + +--FILE-- +format(1, 2); + +var_dump(intl_get_error_code()); +var_dump(intl_get_error_message()); +?> +--EXPECT-- +bool(true) +int(0) +string(12) "U_ZERO_ERROR" +int(0) +string(12) "U_ZERO_ERROR" From 5206ff33ca21be3bcbe5e402fcb856978ff457df Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Mon, 10 Aug 2026 10:04:23 +0200 Subject: [PATCH 3/4] Zend: compile time assert on Bucket size (#23079) In zend_compile.c, flags are stored in the lower bits of the Bucket address. If Bucket is aligned to 8 bytes, the lower three bits are always zero and this gives no problems. If the Bucket is not aligned, this results in non-obvious errors because the memory address and the flags overlap. This is difficult to debug when it happens, so add this assertion to make it more obvious what is wrong. The flags are ZEND_BIND_REF, ZEND_BIND_IMPLICIT, ZEND_BIND_EXPLICIT. Related to GH-19079 --- Zend/zend_compile.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index a2f126fb101d..882b1bf990bf 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -5916,6 +5916,8 @@ static void zend_compile_static_var_common(zend_string *var_name, zval *value, u opline = zend_emit_op(NULL, ZEND_BIND_STATIC, NULL, NULL); opline->op1_type = IS_CV; opline->op1.var = lookup_cv(var_name); + + ZEND_STATIC_ASSERT(sizeof(Bucket) % 8 == 0, "Bucket size not compatible with storing flags in lower three bits"); opline->extended_value = (uint32_t)((char*)value - (char*)CG(active_op_array)->static_variables->arData) | mode; } /* }}} */ From 77170ee6ee26e4fc8078629e6e8720202769ca45 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Mon, 10 Aug 2026 13:07:44 +0500 Subject: [PATCH 4/4] Add a stack limit check in zend_hash_compare() (#23090) Comparing two deeply nested arrays recurses through zend_compare_arrays -> zend_compare_symbol_tables -> zend_hash_compare once per nesting level, and nothing bounds that recursion. zend_hash_compare() only guards against cycles, so a non-cyclic array a few tens of thousands of levels deep runs the C stack out and the process dies with a segfault. === crashes the same way through zend_is_identical(). Both now check the stack limit before descending and throw an Error instead, the same way zend_std_compare_objects() already handles the object case. Fixes GH-23088 --- NEWS | 4 ++++ Zend/tests/gh18572.phpt | 2 +- Zend/tests/gh23088.phpt | 40 ++++++++++++++++++++++++++++++++++++++++ Zend/zend_hash.c | 7 +++++++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 Zend/tests/gh23088.phpt diff --git a/NEWS b/NEWS index 05e3a23118d2..7a93e72de06f 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/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 new file mode 100644 index 000000000000..59153a1f2ba3 --- /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 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.