Skip to content

Commit 8ce7f7f

Browse files
lazergdevnexen
authored andcommitted
Fix GH-23204: use-after-free when __toString() destroys an array argument
implode() walks the array with ZEND_HASH_FOREACH_VAL while holding no reference on it. Converting a Stringable element runs user code, and if that code drops the last remaining reference to the array (`$a = null;` from __toString()), arData is freed and the next iteration reads freed memory. strtr() and str_replace() read their array arguments the same way and crash the same way, so they are fixed here too. Taking a reference on the table for the duration of the read keeps it alive and turns an in-place mutation into a separation instead, same as zend_compare_symbol_tables() does around zend_hash_compare(). In implode() the reference is released after the pieces have been concatenated, since the collected zend_strings are still owned by the array until then. Close GH-23207
1 parent c6d1957 commit 8ce7f7f

2 files changed

Lines changed: 143 additions & 2 deletions

File tree

ext/standard/string.c

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,9 @@ PHPAPI void php_implode(const zend_string *glue, HashTable *pieces, zval *return
983983

984984
uint32_t flags = ZSTR_GET_COPYABLE_CONCAT_PROPERTIES(glue);
985985

986+
/* Converting an element may call __toString(), which can destroy pieces. */
987+
GC_TRY_ADDREF(pieces);
988+
986989
ZEND_HASH_FOREACH_VAL(pieces, tmp) {
987990
if (EXPECTED(Z_TYPE_P(tmp) == IS_STRING)) {
988991
ptr->str = Z_STR_P(tmp);
@@ -1042,6 +1045,7 @@ PHPAPI void php_implode(const zend_string *glue, HashTable *pieces, zval *return
10421045
}
10431046

10441047
free_alloca(strings, use_heap);
1048+
GC_TRY_DTOR_NO_REF(pieces);
10451049
RETURN_NEW_STR(str);
10461050
}
10471051
/* }}} */
@@ -3392,7 +3396,12 @@ static void php_strtr_array(zval *return_value, zend_string *str, HashTable *fro
33923396
{
33933397
if (zend_hash_num_elements(from_ht) < 1) {
33943398
RETURN_STR_COPY(str);
3395-
} else if (zend_hash_num_elements(from_ht) == 1) {
3399+
}
3400+
3401+
/* Converting a replacement may call __toString(), which can destroy from_ht. */
3402+
GC_TRY_ADDREF(from_ht);
3403+
3404+
if (zend_hash_num_elements(from_ht) == 1) {
33963405
zend_long num_key;
33973406
zend_string *str_key, *tmp_str, *replace, *tmp_replace;
33983407
zval *entry;
@@ -3421,11 +3430,13 @@ static void php_strtr_array(zval *return_value, zend_string *str, HashTable *fro
34213430
}
34223431
zend_tmp_string_release(tmp_str);
34233432
zend_tmp_string_release(tmp_replace);
3424-
return;
3433+
break;
34253434
} ZEND_HASH_FOREACH_END();
34263435
} else {
34273436
php_strtr_array_ex(return_value, str, from_ht);
34283437
}
3438+
3439+
GC_TRY_DTOR_NO_REF(from_ht);
34293440
}
34303441

34313442
/* {{{ Translates characters in str using given translation tables */
@@ -4485,6 +4496,17 @@ static void _php_str_replace_common(
44854496
RETURN_THROWS();
44864497
}
44874498

4499+
/* Converting an element may call __toString(), which can destroy the arrays. */
4500+
if (search_ht) {
4501+
GC_TRY_ADDREF(search_ht);
4502+
}
4503+
if (replace_ht) {
4504+
GC_TRY_ADDREF(replace_ht);
4505+
}
4506+
if (subject_ht) {
4507+
GC_TRY_ADDREF(subject_ht);
4508+
}
4509+
44884510
/* if subject is an array */
44894511
if (subject_ht) {
44904512
array_init(return_value);
@@ -4511,6 +4533,16 @@ static void _php_str_replace_common(
45114533
if (zcount) {
45124534
ZEND_TRY_ASSIGN_REF_LONG(zcount, count);
45134535
}
4536+
4537+
if (search_ht) {
4538+
GC_TRY_DTOR_NO_REF(search_ht);
4539+
}
4540+
if (replace_ht) {
4541+
GC_TRY_DTOR_NO_REF(replace_ht);
4542+
}
4543+
if (subject_ht) {
4544+
GC_TRY_DTOR_NO_REF(subject_ht);
4545+
}
45144546
}
45154547

45164548
/* {{{ php_str_replace_common */
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
--TEST--
2+
GH-23204 (Use-after-free when __toString() destroys the array being read)
3+
--CREDITS--
4+
e1abrador
5+
--FILE--
6+
<?php
7+
class Unset_ implements Stringable {
8+
public function __toString(): string {
9+
global $a;
10+
$a = null;
11+
return "X";
12+
}
13+
}
14+
15+
$a = [new Unset_, 2, 3, 4];
16+
echo "destroyed: ", implode(",", $a), "\n";
17+
var_dump($a);
18+
19+
class Append implements Stringable {
20+
public function __toString(): string {
21+
global $b;
22+
$b[] = str_repeat("y", 32);
23+
return "X";
24+
}
25+
}
26+
27+
$b = [new Append, 2, 3, 4];
28+
echo "appended: ", implode(",", $b), "\n";
29+
echo "count: ", count($b), "\n";
30+
31+
class Boom implements Stringable {
32+
public function __toString(): string {
33+
global $c;
34+
$c = null;
35+
throw new Exception("boom");
36+
}
37+
}
38+
39+
$c = [new Boom, 2, 3, 4];
40+
try {
41+
implode(",", $c);
42+
} catch (Exception $e) {
43+
echo $e::class, ': ', $e->getMessage(), "\n";
44+
}
45+
46+
class UnsetPats implements Stringable {
47+
public function __toString(): string {
48+
global $d;
49+
$d = null;
50+
return "X";
51+
}
52+
}
53+
54+
$d = ["aa" => new UnsetPats, "bb" => "2", "cc" => "3", "dd" => "4"];
55+
echo "strtr: ", strtr("aabbccdd", $d), "\n";
56+
57+
$e = ["aa" => new UnsetPats];
58+
$d = &$e;
59+
echo "strtr single: ", strtr("aabb", $e), "\n";
60+
61+
class UnsetSearch implements Stringable {
62+
public function __toString(): string {
63+
global $f;
64+
$f = null;
65+
return "a";
66+
}
67+
}
68+
69+
$f = [new UnsetSearch, "b", "c", "d"];
70+
echo "str_replace search: ", str_replace($f, "z", "abcd"), "\n";
71+
72+
class UnsetReplace implements Stringable {
73+
public function __toString(): string {
74+
global $g;
75+
$g = null;
76+
return "z";
77+
}
78+
}
79+
80+
$g = [new UnsetReplace, "y", "y", "y"];
81+
echo "str_replace replace: ", str_replace(["a", "b", "c", "d"], $g, "abcd"), "\n";
82+
83+
class UnsetSubject implements Stringable {
84+
public function __toString(): string {
85+
global $h;
86+
$h = null;
87+
return "abcd";
88+
}
89+
}
90+
91+
$h = [new UnsetSubject, "abcd"];
92+
var_dump(str_replace("a", "z", $h));
93+
?>
94+
--EXPECT--
95+
destroyed: X,2,3,4
96+
NULL
97+
appended: X,2,3,4
98+
count: 5
99+
Exception: boom
100+
strtr: X234
101+
strtr single: Xbb
102+
str_replace search: zzzz
103+
str_replace replace: zyyy
104+
array(2) {
105+
[0]=>
106+
string(4) "zbcd"
107+
[1]=>
108+
string(4) "zbcd"
109+
}

0 commit comments

Comments
 (0)