Commit 2c112e36960 for php.net
commit 2c112e369602362dc0241979832aadf771902a15
Author: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com>
Date: Wed Jan 14 20:07:11 2026 +0100
Fix GH-20836: Stack overflow in mb_convert_variables with recursive array references (#20839)
diff --git a/NEWS b/NEWS
index 761d4b8b983..20d66b445e4 100644
--- a/NEWS
+++ b/NEWS
@@ -16,6 +16,8 @@ PHP NEWS
- MbString:
. Fixed bug GH-20833 (mb_str_pad() divide by zero if padding string is
invalid in the encoding). (ndossche)
+ . Fixed bug GH-20836 (Stack overflow in mb_convert_variables with
+ recursive array references). (alexandre-daubois)
- Phar:
. Fixed bug GH-20882 (buildFromIterator breaks with missing base directory).
diff --git a/ext/mbstring/mbstring.c b/ext/mbstring/mbstring.c
index 1491e1728cd..b320a6a5f0e 100644
--- a/ext/mbstring/mbstring.c
+++ b/ext/mbstring/mbstring.c
@@ -3691,11 +3691,26 @@ 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;
ZVAL_DEREF(var);
+ if (mb_check_stack_limit()) {
+ return 0;
+ }
+
if (Z_TYPE_P(var) == IS_STRING) {
count++;
} else if (Z_TYPE_P(var) == IS_ARRAY || Z_TYPE_P(var) == IS_OBJECT) {
@@ -3726,6 +3741,10 @@ static bool mb_recursive_find_strings(zval *var, const unsigned char **val_list,
{
ZVAL_DEREF(var);
+ if (mb_check_stack_limit()) {
+ return true;
+ }
+
if (Z_TYPE_P(var) == IS_STRING) {
val_list[*count] = (const unsigned char*)Z_STRVAL_P(var);
len_list[*count] = Z_STRLEN_P(var);
@@ -3763,6 +3782,10 @@ static bool mb_recursive_convert_variable(zval *var, const mbfl_encoding* from_e
{
zval *entry, *orig_var;
+ if (mb_check_stack_limit()) {
+ return true;
+ }
+
orig_var = var;
ZVAL_DEREF(var);
@@ -3771,17 +3794,25 @@ static bool mb_recursive_convert_variable(zval *var, const mbfl_encoding* from_e
zval_ptr_dtor(orig_var);
ZVAL_STR(orig_var, ret);
} else if (Z_TYPE_P(var) == IS_ARRAY || Z_TYPE_P(var) == IS_OBJECT) {
- if (Z_TYPE_P(var) == IS_ARRAY) {
- SEPARATE_ARRAY(var);
- }
- if (Z_REFCOUNTED_P(var)) {
- if (Z_IS_RECURSIVE_P(var)) {
+ HashTable *ht = HASH_OF(var);
+ HashTable *orig_ht = ht;
+
+ if (ht) {
+ if (GC_IS_RECURSIVE(ht)) {
return true;
}
- Z_PROTECT_RECURSION_P(var);
+
+ GC_TRY_PROTECT_RECURSION(ht);
}
- HashTable *ht = HASH_OF(var);
+ if (Z_TYPE_P(var) == IS_ARRAY) {
+ SEPARATE_ARRAY(var);
+ ht = Z_ARRVAL_P(var);
+
+ if (ht && ht != orig_ht && !GC_IS_RECURSIVE(ht)) {
+ GC_TRY_PROTECT_RECURSION(ht);
+ }
+ }
if (ht != NULL) {
ZEND_HASH_FOREACH_VAL(ht, entry) {
/* Can be a typed property declaration, in which case we need to remove the reference from the source list.
@@ -3800,16 +3831,22 @@ static bool mb_recursive_convert_variable(zval *var, const mbfl_encoding* from_e
}
if (mb_recursive_convert_variable(entry, from_encoding, to_encoding)) {
- if (Z_REFCOUNTED_P(var)) {
- Z_UNPROTECT_RECURSION_P(var);
+ if (ht && ht != orig_ht) {
+ GC_TRY_UNPROTECT_RECURSION(ht);
+ }
+ if (orig_ht) {
+ GC_TRY_UNPROTECT_RECURSION(orig_ht);
}
return true;
}
} ZEND_HASH_FOREACH_END();
}
- if (Z_REFCOUNTED_P(var)) {
- Z_UNPROTECT_RECURSION_P(var);
+ if (ht && ht != orig_ht) {
+ GC_TRY_UNPROTECT_RECURSION(ht);
+ }
+ if (orig_ht) {
+ GC_TRY_UNPROTECT_RECURSION(orig_ht);
}
}
@@ -3883,7 +3920,9 @@ PHP_FUNCTION(mb_convert_variables)
efree(ZEND_VOIDP(elist));
efree(ZEND_VOIDP(val_list));
efree(len_list);
- php_error_docref(NULL, E_WARNING, "Cannot handle recursive references");
+ if (!EG(exception)) {
+ php_error_docref(NULL, E_WARNING, "Cannot handle recursive references");
+ }
RETURN_FALSE;
}
}
@@ -3905,7 +3944,9 @@ PHP_FUNCTION(mb_convert_variables)
zval *zv = &args[n];
ZVAL_DEREF(zv);
if (mb_recursive_convert_variable(zv, from_encoding, to_encoding)) {
- php_error_docref(NULL, E_WARNING, "Cannot handle recursive references");
+ if (!EG(exception)) {
+ php_error_docref(NULL, E_WARNING, "Cannot handle recursive references");
+ }
RETURN_FALSE;
}
}
diff --git a/ext/mbstring/tests/gh20836.phpt b/ext/mbstring/tests/gh20836.phpt
new file mode 100644
index 00000000000..60b6a4ce8d4
--- /dev/null
+++ b/ext/mbstring/tests/gh20836.phpt
@@ -0,0 +1,33 @@
+--TEST--
+GH-20836 (Stack overflow in mb_convert_variables with recursive array references)
+--EXTENSIONS--
+mbstring
+--FILE--
+<?php
+
+$a = [];
+$b = [];
+$a[] = $b[] = &$a;
+var_dump(mb_convert_variables('utf-8', 'utf-8', $a));
+
+$c = [];
+$c[] = &$c;
+var_dump(mb_convert_variables('utf-8', 'utf-8', $c));
+
+$normal = ['test', 'array'];
+var_dump(mb_convert_variables('utf-8', 'utf-8', $normal));
+
+$d = ['level1' => ['level2' => ['level3' => 'data']]];
+var_dump(mb_convert_variables('utf-8', 'utf-8', $d));
+
+echo "Done\n";
+?>
+--EXPECTF--
+Warning: mb_convert_variables(): Cannot handle recursive references in %s on line %d
+bool(false)
+
+Warning: mb_convert_variables(): Cannot handle recursive references in %s on line %d
+bool(false)
+string(5) "UTF-8"
+string(5) "UTF-8"
+Done
diff --git a/ext/mbstring/tests/gh20836_stack_limit.phpt b/ext/mbstring/tests/gh20836_stack_limit.phpt
new file mode 100644
index 00000000000..b58833ff80c
--- /dev/null
+++ b/ext/mbstring/tests/gh20836_stack_limit.phpt
@@ -0,0 +1,38 @@
+--TEST--
+GH-20836 (Stack overflow in mb_convert_variables with recursive array references, stack limit case)
+--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=128K
+--FILE--
+<?php
+
+function createDeepArray($depth) {
+ if ($depth <= 0) {
+ return 'deep value';
+ }
+ return ['nested' => createDeepArray($depth - 1)];
+}
+
+// Create a deeply nested array that will trigger stack limit
+$deepArray = createDeepArray(15000);
+
+mb_convert_variables('utf-8', 'utf-8', $deepArray);
+
+echo "Done\n";
+?>
+--EXPECTF--
+Fatal error: Uncaught Error: Maximum call stack size of %d bytes (zend.max_allowed_stack_size - zend.reserved_stack_size) reached. Infinite recursion? in %s:%d
+Stack trace:
+#0 %s(%d): mb_convert_variables('utf-8', 'utf-8', Array)
+#1 {main}
+ thrown in %s on line %d