Skip to content

Commit 3280368

Browse files
Merge branch 'PHP-8.4' into PHP-8.5
* PHP-8.4: Fix phpGH-20836: Stack overflow in mb_convert_variables with recursive array references (php#20839)
2 parents 695df88 + 2c112e3 commit 3280368

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
@@ -22,6 +22,8 @@ PHP NEWS
2222
- MbString:
2323
. Fixed bug GH-20833 (mb_str_pad() divide by zero if padding string is
2424
invalid in the encoding). (ndossche)
25+
. Fixed bug GH-20836 (Stack overflow in mb_convert_variables with
26+
recursive array references). (alexandre-daubois)
2527

2628
- Phar:
2729
. Fixed bug GH-20882 (buildFromIterator breaks with missing base directory).

ext/mbstring/mbstring.c

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3697,11 +3697,26 @@ PHP_FUNCTION(mb_convert_kana)
36973697
RETVAL_STR(jp_kana_convert(str, enc, opt));
36983698
}
36993699

3700+
static zend_always_inline bool mb_check_stack_limit(void)
3701+
{
3702+
#ifdef ZEND_CHECK_STACK_LIMIT
3703+
if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) {
3704+
zend_call_stack_size_error();
3705+
return true;
3706+
}
3707+
#endif
3708+
return false;
3709+
}
3710+
37003711
static unsigned int mb_recursive_count_strings(zval *var)
37013712
{
37023713
unsigned int count = 0;
37033714
ZVAL_DEREF(var);
37043715

3716+
if (mb_check_stack_limit()) {
3717+
return 0;
3718+
}
3719+
37053720
if (Z_TYPE_P(var) == IS_STRING) {
37063721
count++;
37073722
} else if (Z_TYPE_P(var) == IS_ARRAY || Z_TYPE_P(var) == IS_OBJECT) {
@@ -3732,6 +3747,10 @@ static bool mb_recursive_find_strings(zval *var, const unsigned char **val_list,
37323747
{
37333748
ZVAL_DEREF(var);
37343749

3750+
if (mb_check_stack_limit()) {
3751+
return true;
3752+
}
3753+
37353754
if (Z_TYPE_P(var) == IS_STRING) {
37363755
val_list[*count] = (const unsigned char*)Z_STRVAL_P(var);
37373756
len_list[*count] = Z_STRLEN_P(var);
@@ -3769,6 +3788,10 @@ static bool mb_recursive_convert_variable(zval *var, const mbfl_encoding* from_e
37693788
{
37703789
zval *entry, *orig_var;
37713790

3791+
if (mb_check_stack_limit()) {
3792+
return true;
3793+
}
3794+
37723795
orig_var = var;
37733796
ZVAL_DEREF(var);
37743797

@@ -3777,17 +3800,25 @@ static bool mb_recursive_convert_variable(zval *var, const mbfl_encoding* from_e
37773800
zval_ptr_dtor(orig_var);
37783801
ZVAL_STR(orig_var, ret);
37793802
} else if (Z_TYPE_P(var) == IS_ARRAY || Z_TYPE_P(var) == IS_OBJECT) {
3780-
if (Z_TYPE_P(var) == IS_ARRAY) {
3781-
SEPARATE_ARRAY(var);
3782-
}
3783-
if (Z_REFCOUNTED_P(var)) {
3784-
if (Z_IS_RECURSIVE_P(var)) {
3803+
HashTable *ht = HASH_OF(var);
3804+
HashTable *orig_ht = ht;
3805+
3806+
if (ht) {
3807+
if (GC_IS_RECURSIVE(ht)) {
37853808
return true;
37863809
}
3787-
Z_PROTECT_RECURSION_P(var);
3810+
3811+
GC_TRY_PROTECT_RECURSION(ht);
37883812
}
37893813

3790-
HashTable *ht = HASH_OF(var);
3814+
if (Z_TYPE_P(var) == IS_ARRAY) {
3815+
SEPARATE_ARRAY(var);
3816+
ht = Z_ARRVAL_P(var);
3817+
3818+
if (ht && ht != orig_ht && !GC_IS_RECURSIVE(ht)) {
3819+
GC_TRY_PROTECT_RECURSION(ht);
3820+
}
3821+
}
37913822
if (ht != NULL) {
37923823
ZEND_HASH_FOREACH_VAL(ht, entry) {
37933824
/* Can be a typed property declaration, in which case we need to remove the reference from the source list.
@@ -3806,16 +3837,22 @@ static bool mb_recursive_convert_variable(zval *var, const mbfl_encoding* from_e
38063837
}
38073838

38083839
if (mb_recursive_convert_variable(entry, from_encoding, to_encoding)) {
3809-
if (Z_REFCOUNTED_P(var)) {
3810-
Z_UNPROTECT_RECURSION_P(var);
3840+
if (ht && ht != orig_ht) {
3841+
GC_TRY_UNPROTECT_RECURSION(ht);
3842+
}
3843+
if (orig_ht) {
3844+
GC_TRY_UNPROTECT_RECURSION(orig_ht);
38113845
}
38123846
return true;
38133847
}
38143848
} ZEND_HASH_FOREACH_END();
38153849
}
38163850

3817-
if (Z_REFCOUNTED_P(var)) {
3818-
Z_UNPROTECT_RECURSION_P(var);
3851+
if (ht && ht != orig_ht) {
3852+
GC_TRY_UNPROTECT_RECURSION(ht);
3853+
}
3854+
if (orig_ht) {
3855+
GC_TRY_UNPROTECT_RECURSION(orig_ht);
38193856
}
38203857
}
38213858

@@ -3889,7 +3926,9 @@ PHP_FUNCTION(mb_convert_variables)
38893926
efree(ZEND_VOIDP(elist));
38903927
efree(ZEND_VOIDP(val_list));
38913928
efree(len_list);
3892-
php_error_docref(NULL, E_WARNING, "Cannot handle recursive references");
3929+
if (!EG(exception)) {
3930+
php_error_docref(NULL, E_WARNING, "Cannot handle recursive references");
3931+
}
38933932
RETURN_FALSE;
38943933
}
38953934
}
@@ -3911,7 +3950,9 @@ PHP_FUNCTION(mb_convert_variables)
39113950
zval *zv = &args[n];
39123951
ZVAL_DEREF(zv);
39133952
if (mb_recursive_convert_variable(zv, from_encoding, to_encoding)) {
3914-
php_error_docref(NULL, E_WARNING, "Cannot handle recursive references");
3953+
if (!EG(exception)) {
3954+
php_error_docref(NULL, E_WARNING, "Cannot handle recursive references");
3955+
}
39153956
RETURN_FALSE;
39163957
}
39173958
}

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=128K
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(15000);
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)