Commit 49b9884355c for php

commit 49b9884355ca1ada41c50d090e024d5a12cb6979
Author: Eyüp Can Akman <eyupcanakman@gmail.com>
Date:   Thu Nov 5 19:03:05 2026 +0300

    Fix stack overflow in mb_check_encoding() and mb_convert_encoding()

    The array walk in both functions goes one C call deeper per nesting level without checking the stack limit, so a deeply nested array crashes the process.
    Call mb_check_stack_limit() in the two recursive helpers, as mb_convert_variables() already does.

diff --git a/NEWS b/NEWS
index 3c5ffd7802b..25a261cb17c 100644
--- a/NEWS
+++ b/NEWS
@@ -12,6 +12,10 @@ PHP                                                                        NEWS
 - Curl:
   . Fixed curl_reset() retaining CURLOPT_PRIVATE after reset. (lacatoire)

+- MBString:
+  . Fixed stack overflow in mb_check_encoding() and mb_convert_encoding() with
+    deeply nested arrays. (Eyüp Can Akman)
+
 - Opcache:
   . Fixed bug GH-25165 (Tracing JIT loops forever once opcache.jit_buffer_size
     is exhausted while compiling side traces). (Ilia Alshanetsky)
diff --git a/ext/mbstring/mbstring.c b/ext/mbstring/mbstring.c
index fdcd2ef6199..3df3367de07 100644
--- a/ext/mbstring/mbstring.c
+++ b/ext/mbstring/mbstring.c
@@ -2776,6 +2776,17 @@ MBSTRING_API zend_string* php_mb_convert_encoding(const char *input, size_t leng
 	return php_mb_convert_encoding_ex(input, length, to_encoding, from_encoding);
 }

+static zend_always_inline bool mb_check_stack_limit(void)
+{
+#ifdef ZEND_CHECK_STACK_LIMIT
+	if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) {
+		zend_call_stack_size_error();
+		return true;
+	}
+#endif
+	return false;
+}
+
 MBSTRING_API HashTable *php_mb_convert_encoding_recursive(HashTable *input, const mbfl_encoding *to_encoding, const mbfl_encoding **from_encodings, size_t num_from_encodings)
 {
 	HashTable *output, *chash;
@@ -2787,6 +2798,10 @@ MBSTRING_API HashTable *php_mb_convert_encoding_recursive(HashTable *input, cons
 		return NULL;
 	}

+	if (mb_check_stack_limit()) {
+		return NULL;
+	}
+
 	if (GC_IS_RECURSIVE(input)) {
 		GC_UNPROTECT_RECURSION(input);
 		php_error_docref(NULL, E_WARNING, "Cannot convert recursively referenced values");
@@ -2830,6 +2845,13 @@ MBSTRING_API HashTable *php_mb_convert_encoding_recursive(HashTable *input, cons
 					Z_ARRVAL_P(entry), to_encoding, from_encodings, num_from_encodings);
 				if (chash) {
 					ZVAL_ARR(&entry_tmp, chash);
+				} else if (UNEXPECTED(EG(exception))) {
+					if (key) {
+						zend_string_release(key);
+					}
+					GC_TRY_UNPROTECT_RECURSION(input);
+					zend_array_destroy(output);
+					return NULL;
 				} else {
 					ZVAL_EMPTY_ARRAY(&entry_tmp);
 				}
@@ -2935,7 +2957,9 @@ PHP_FUNCTION(mb_convert_encoding)
 		HashTable *tmp;
 		tmp = php_mb_convert_encoding_recursive(
 			input_ht, to_encoding, from_encodings, num_from_encodings);
-		RETVAL_ARR(tmp);
+		if (tmp) {
+			RETVAL_ARR(tmp);
+		}
 	}

 	if (free_from_encodings) {
@@ -3737,17 +3761,6 @@ PHP_FUNCTION(mb_convert_kana)
 	RETVAL_STR(jp_kana_convert(str, enc, opt));
 }

-static zend_always_inline bool mb_check_stack_limit(void)
-{
-#ifdef ZEND_CHECK_STACK_LIMIT
-	if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) {
-		zend_call_stack_size_error();
-		return true;
-	}
-#endif
-	return false;
-}
-
 static unsigned int mb_recursive_count_strings(zval *var)
 {
 	unsigned int count = 0;
@@ -5677,6 +5690,10 @@ static bool php_mb_check_encoding_recursive(HashTable *vars, const mbfl_encoding

 	(void)(idx); /* Suppress spurious compiler warning that `idx` is not used */

+	if (mb_check_stack_limit()) {
+		return false;
+	}
+
 	if (GC_IS_RECURSIVE(vars)) {
 		php_error_docref(NULL, E_WARNING, "Cannot not handle circular references");
 		return false;
@@ -5699,6 +5716,10 @@ static bool php_mb_check_encoding_recursive(HashTable *vars, const mbfl_encoding
 				break;
 			case IS_ARRAY:
 				if (!php_mb_check_encoding_recursive(Z_ARRVAL_P(entry), encoding)) {
+					if (UNEXPECTED(EG(exception))) {
+						GC_TRY_UNPROTECT_RECURSION(vars);
+						return false;
+					}
 					valid = false;
 					break;
 				}
diff --git a/ext/mbstring/tests/mb_encoding_recursive_stack_limit.phpt b/ext/mbstring/tests/mb_encoding_recursive_stack_limit.phpt
new file mode 100644
index 00000000000..146dcffec74
--- /dev/null
+++ b/ext/mbstring/tests/mb_encoding_recursive_stack_limit.phpt
@@ -0,0 +1,52 @@
+--TEST--
+Stack overflow in mb_check_encoding() and mb_convert_encoding() with deeply nested arrays
+--EXTENSIONS--
+mbstring
+--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 nested arrays, so the second must not be visited after the first throws. */
+foreach ([[$deep], [$deep, $deep]] as $input) {
+    try {
+        mb_check_encoding($input, 'UTF-8');
+    } catch (Error $e) {
+        echo $e->getMessage(), "\n";
+        var_dump($e->getPrevious());
+    }
+    try {
+        mb_convert_encoding($input, 'UTF-8', 'ISO-8859-1');
+    } catch (Error $e) {
+        echo $e->getMessage(), "\n";
+        var_dump($e->getPrevious());
+    }
+}
+
+unset($input, $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
+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