Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ PHP NEWS
nested arrays). (Lazizbek Ergashev)
. Fixed bug GH-23115 (Stack overflow in compact() with deeply nested
arrays). (Lazizbek Ergashev)
. Fixed bug GH-23204 (Use-after-free in implode(), strtr() and str_replace()
when __toString() destroys an array argument). (Lazizbek Ergashev)

- Streams:
. Fixed bug GH-15836 (Use-after-free when a user stream filter accesses
Expand Down
36 changes: 34 additions & 2 deletions ext/standard/string.c
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,9 @@ PHPAPI void php_implode(const zend_string *glue, HashTable *pieces, zval *return

uint32_t flags = ZSTR_GET_COPYABLE_CONCAT_PROPERTIES(glue);

/* Converting an element may call __toString(), which can destroy pieces. */
GC_TRY_ADDREF(pieces);
Comment thread
lazerg marked this conversation as resolved.

ZEND_HASH_FOREACH_VAL(pieces, tmp) {
if (EXPECTED(Z_TYPE_P(tmp) == IS_STRING)) {
ptr->str = Z_STR_P(tmp);
Expand Down Expand Up @@ -1042,6 +1045,7 @@ PHPAPI void php_implode(const zend_string *glue, HashTable *pieces, zval *return
}

free_alloca(strings, use_heap);
GC_TRY_DTOR_NO_REF(pieces);
RETURN_NEW_STR(str);
}
/* }}} */
Expand Down Expand Up @@ -3392,7 +3396,12 @@ static void php_strtr_array(zval *return_value, zend_string *str, HashTable *fro
{
if (zend_hash_num_elements(from_ht) < 1) {
RETURN_STR_COPY(str);
} else if (zend_hash_num_elements(from_ht) == 1) {
}

/* Converting a replacement may call __toString(), which can destroy from_ht. */
GC_TRY_ADDREF(from_ht);

if (zend_hash_num_elements(from_ht) == 1) {
zend_long num_key;
zend_string *str_key, *tmp_str, *replace, *tmp_replace;
zval *entry;
Expand Down Expand Up @@ -3421,11 +3430,13 @@ static void php_strtr_array(zval *return_value, zend_string *str, HashTable *fro
}
zend_tmp_string_release(tmp_str);
zend_tmp_string_release(tmp_replace);
return;
break;
} ZEND_HASH_FOREACH_END();
} else {
php_strtr_array_ex(return_value, str, from_ht);
}

GC_TRY_DTOR_NO_REF(from_ht);
}

/* {{{ Translates characters in str using given translation tables */
Expand Down Expand Up @@ -4485,6 +4496,17 @@ static void _php_str_replace_common(
RETURN_THROWS();
}

/* Converting an element may call __toString(), which can destroy the arrays. */
if (search_ht) {
GC_TRY_ADDREF(search_ht);
}
if (replace_ht) {
GC_TRY_ADDREF(replace_ht);
}
if (subject_ht) {
GC_TRY_ADDREF(subject_ht);
}

/* if subject is an array */
if (subject_ht) {
array_init(return_value);
Expand All @@ -4511,6 +4533,16 @@ static void _php_str_replace_common(
if (zcount) {
ZEND_TRY_ASSIGN_REF_LONG(zcount, count);
}

if (search_ht) {
GC_TRY_DTOR_NO_REF(search_ht);
}
if (replace_ht) {
GC_TRY_DTOR_NO_REF(replace_ht);
}
if (subject_ht) {
GC_TRY_DTOR_NO_REF(subject_ht);
}
}

/* {{{ php_str_replace_common */
Expand Down
109 changes: 109 additions & 0 deletions ext/standard/tests/strings/gh23204.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
--TEST--
GH-23204 (Use-after-free when __toString() destroys the array being read)
--CREDITS--
e1abrador
--FILE--

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please credit the reporter within --CREDITS--.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, credited as iluuu1994.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not me, @e1abrador.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs say the CREDITS section should no longer be used. Should the wording be extended with a "unless reporters have to be credited"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My mistake, sorry. Changed to e1abrador.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs say the CREDITS section should no longer be used. Should the wording be extended with a "unless reporters have to be credited"?

The reasoning behind this is that git commits can track authors via Co-authored-by, which is complete bs. CREDITS section is a much more precise indication of authorship than a commit-wide author tag. So I purposefully ignore that docs and I believe that line should be removed.

@arnaud-lb arnaud-lb Aug 11, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like that this paragraph of the docs is only considering the case of the commit authors crediting themselves. It makes sense to me that no CREDITS section should be used in this case.

We could expand this paragraph with the case of committing a reproducer that was contributed by someone else.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I proposed a change in #23215

<?php
class Unset_ implements Stringable {
public function __toString(): string {
global $a;
$a = null;
return "X";
}
}

$a = [new Unset_, 2, 3, 4];
echo "destroyed: ", implode(",", $a), "\n";
var_dump($a);

class Append implements Stringable {
public function __toString(): string {
global $b;
$b[] = str_repeat("y", 32);
return "X";
}
}

$b = [new Append, 2, 3, 4];
echo "appended: ", implode(",", $b), "\n";
echo "count: ", count($b), "\n";

class Boom implements Stringable {
public function __toString(): string {
global $c;
$c = null;
throw new Exception("boom");
}
}

$c = [new Boom, 2, 3, 4];
try {
implode(",", $c);
} catch (Exception $e) {
echo $e::class, ': ', $e->getMessage(), "\n";
}

class UnsetPats implements Stringable {
public function __toString(): string {
global $d;
$d = null;
return "X";
}
}

$d = ["aa" => new UnsetPats, "bb" => "2", "cc" => "3", "dd" => "4"];
echo "strtr: ", strtr("aabbccdd", $d), "\n";

$e = ["aa" => new UnsetPats];
$d = &$e;
echo "strtr single: ", strtr("aabb", $e), "\n";

class UnsetSearch implements Stringable {
public function __toString(): string {
global $f;
$f = null;
return "a";
}
}

$f = [new UnsetSearch, "b", "c", "d"];
echo "str_replace search: ", str_replace($f, "z", "abcd"), "\n";

class UnsetReplace implements Stringable {
public function __toString(): string {
global $g;
$g = null;
return "z";
}
}

$g = [new UnsetReplace, "y", "y", "y"];
echo "str_replace replace: ", str_replace(["a", "b", "c", "d"], $g, "abcd"), "\n";

class UnsetSubject implements Stringable {
public function __toString(): string {
global $h;
$h = null;
return "abcd";
}
}

$h = [new UnsetSubject, "abcd"];
var_dump(str_replace("a", "z", $h));
?>
--EXPECT--
destroyed: X,2,3,4
NULL
appended: X,2,3,4
count: 5
Exception: boom
strtr: X234
strtr single: Xbb
str_replace search: zzzz
str_replace replace: zyyy
array(2) {
[0]=>
string(4) "zbcd"
[1]=>
string(4) "zbcd"
}
Loading