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