Skip to content

Commit b18ee3b

Browse files
Fix GH-20836: Stack overflow in mb_convert_variables with recursive array references
1 parent 226f68b commit b18ee3b

File tree

4 files changed

+127
-13
lines changed

4 files changed

+127
-13
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ PHP NEWS
55
- MbString:
66
. Fixed bug GH-20833 (mb_str_pad() divide by zero if padding string is
77
invalid in the encoding). (ndossche)
8+
. Fixed bug GH-20836 (Stack overflow in mb_convert_variables with
9+
recursive array references). (alexandre-daubois)
810

911
- Readline:
1012
. Fixed bug GH-18139 (Memory leak when overriding some settings

ext/mbstring/mbstring.c

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3691,11 +3691,26 @@ PHP_FUNCTION(mb_convert_kana)
36913691
RETVAL_STR(jp_kana_convert(str, enc, opt));
36923692
}
36933693

3694+
static zend_always_inline bool mb_check_stack_limit(void)
3695+
{
3696+
#ifdef ZEND_CHECK_STACK_LIMIT
3697+
if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) {
3698+
zend_call_stack_size_error();
3699+
return true;
3700+
}
3701+
#endif
3702+
return false;
3703+
}
3704+
36943705
static unsigned int mb_recursive_count_strings(zval *var)
36953706
{
36963707
unsigned int count = 0;
36973708
ZVAL_DEREF(var);
36983709

3710+
if (mb_check_stack_limit()) {
3711+
return 0;
3712+
}
3713+
36993714
if (Z_TYPE_P(var) == IS_STRING) {
37003715
count++;
37013716
} 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,
37263741
{
37273742
ZVAL_DEREF(var);
37283743

3744+
if (mb_check_stack_limit()) {
3745+
return true;
3746+
}
3747+
37293748
if (Z_TYPE_P(var) == IS_STRING) {
37303749
val_list[*count] = (const unsigned char*)Z_STRVAL_P(var);
37313750
len_list[*count] = Z_STRLEN_P(var);
@@ -3763,6 +3782,10 @@ static bool mb_recursive_convert_variable(zval *var, const mbfl_encoding* from_e
37633782
{
37643783
zval *entry, *orig_var;
37653784

3785+
if (mb_check_stack_limit()) {
3786+
return true;
3787+
}
3788+
37663789
orig_var = var;
37673790
ZVAL_DEREF(var);
37683791

@@ -3771,17 +3794,25 @@ static bool mb_recursive_convert_variable(zval *var, const mbfl_encoding* from_e
37713794
zval_ptr_dtor(orig_var);
37723795
ZVAL_STR(orig_var, ret);
37733796
} else if (Z_TYPE_P(var) == IS_ARRAY || Z_TYPE_P(var) == IS_OBJECT) {
3774-
if (Z_TYPE_P(var) == IS_ARRAY) {
3775-
SEPARATE_ARRAY(var);
3776-
}
3777-
if (Z_REFCOUNTED_P(var)) {
3778-
if (Z_IS_RECURSIVE_P(var)) {
3797+
HashTable *ht = HASH_OF(var);
3798+
HashTable *orig_ht = ht;
3799+
3800+
if (ht) {
3801+
if (GC_IS_RECURSIVE(ht)) {
37793802
return true;
37803803
}
3781-
Z_PROTECT_RECURSION_P(var);
3804+
3805+
GC_TRY_PROTECT_RECURSION(ht);
37823806
}
37833807

3784-
HashTable *ht = HASH_OF(var);
3808+
if (Z_TYPE_P(var) == IS_ARRAY) {
3809+
SEPARATE_ARRAY(var);
3810+
ht = Z_ARRVAL_P(var);
3811+
3812+
if (ht && ht != orig_ht && !GC_IS_RECURSIVE(ht)) {
3813+
GC_TRY_PROTECT_RECURSION(ht);
3814+
}
3815+
}
37853816
if (ht != NULL) {
37863817
ZEND_HASH_FOREACH_VAL(ht, entry) {
37873818
/* 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
38003831
}
38013832

38023833
if (mb_recursive_convert_variable(entry, from_encoding, to_encoding)) {
3803-
if (Z_REFCOUNTED_P(var)) {
3804-
Z_UNPROTECT_RECURSION_P(var);
3834+
if (ht && ht != orig_ht) {
3835+
GC_TRY_UNPROTECT_RECURSION(ht);
3836+
}
3837+
if (orig_ht) {
3838+
GC_TRY_UNPROTECT_RECURSION(orig_ht);
38053839
}
38063840
return true;
38073841
}
38083842
} ZEND_HASH_FOREACH_END();
38093843
}
38103844

3811-
if (Z_REFCOUNTED_P(var)) {
3812-
Z_UNPROTECT_RECURSION_P(var);
3845+
if (ht && ht != orig_ht) {
3846+
GC_TRY_UNPROTECT_RECURSION(ht);
3847+
}
3848+
if (orig_ht) {
3849+
GC_TRY_UNPROTECT_RECURSION(orig_ht);
38133850
}
38143851
}
38153852

@@ -3883,7 +3920,9 @@ PHP_FUNCTION(mb_convert_variables)
38833920
efree(ZEND_VOIDP(elist));
38843921
efree(ZEND_VOIDP(val_list));
38853922
efree(len_list);
3886-
php_error_docref(NULL, E_WARNING, "Cannot handle recursive references");
3923+
if (!EG(exception)) {
3924+
php_error_docref(NULL, E_WARNING, "Cannot handle recursive references");
3925+
}
38873926
RETURN_FALSE;
38883927
}
38893928
}
@@ -3905,7 +3944,9 @@ PHP_FUNCTION(mb_convert_variables)
39053944
zval *zv = &args[n];
39063945
ZVAL_DEREF(zv);
39073946
if (mb_recursive_convert_variable(zv, from_encoding, to_encoding)) {
3908-
php_error_docref(NULL, E_WARNING, "Cannot handle recursive references");
3947+
if (!EG(exception)) {
3948+
php_error_docref(NULL, E_WARNING, "Cannot handle recursive references");
3949+
}
39093950
RETURN_FALSE;
39103951
}
39113952
}

ext/mbstring/tests/gh20836.phpt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
--TEST--
2+
GH-20836 (Stack overflow in mb_convert_variables with recursive array references)
3+
--EXTENSIONS--
4+
mbstring
5+
--FILE--
6+
<?php
7+
8+
$a = [];
9+
$b = [];
10+
$a[] = $b[] = &$a;
11+
var_dump(mb_convert_variables('utf-8', 'utf-8', $a));
12+
13+
$c = [];
14+
$c[] = &$c;
15+
var_dump(mb_convert_variables('utf-8', 'utf-8', $c));
16+
17+
$normal = ['test', 'array'];
18+
var_dump(mb_convert_variables('utf-8', 'utf-8', $normal));
19+
20+
$d = ['level1' => ['level2' => ['level3' => 'data']]];
21+
var_dump(mb_convert_variables('utf-8', 'utf-8', $d));
22+
23+
echo "Done\n";
24+
?>
25+
--EXPECTF--
26+
Warning: mb_convert_variables(): Cannot handle recursive references in %s on line %d
27+
bool(false)
28+
29+
Warning: mb_convert_variables(): Cannot handle recursive references in %s on line %d
30+
bool(false)
31+
string(5) "UTF-8"
32+
string(5) "UTF-8"
33+
Done
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
--TEST--
2+
GH-20836 (Stack overflow in mb_convert_variables with recursive array references) - Stack limit case
3+
--EXTENSIONS--
4+
mbstring
5+
--SKIPIF--
6+
<?php
7+
if (ini_get('zend.max_allowed_stack_size') === false) {
8+
die('skip No stack limit support');
9+
}
10+
if (getenv('SKIP_ASAN')) {
11+
die('skip ASAN needs different stack limit setting due to more stack space usage');
12+
}
13+
?>
14+
--INI--
15+
zend.max_allowed_stack_size=512K
16+
--FILE--
17+
<?php
18+
19+
function createDeepArray($depth) {
20+
if ($depth <= 0) {
21+
return 'deep value';
22+
}
23+
return ['nested' => createDeepArray($depth - 1)];
24+
}
25+
26+
// Create a deeply nested array that will trigger stack limit
27+
$deepArray = createDeepArray(5000);
28+
29+
mb_convert_variables('utf-8', 'utf-8', $deepArray);
30+
31+
echo "Done\n";
32+
?>
33+
--EXPECTF--
34+
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
35+
Stack trace:
36+
#0 %s(%d): mb_convert_variables('utf-8', 'utf-8', Array)
37+
#1 {main}
38+
thrown in %s on line %d

0 commit comments

Comments
 (0)