Skip to content

Commit 396ac4d

Browse files
Fix GH-19999: GC refcount assertion failure when destructor modifies variable during concat
1 parent e029f8f commit 396ac4d

File tree

2 files changed

+44
-7
lines changed

2 files changed

+44
-7
lines changed

Zend/tests/gh19999.phpt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
--TEST--
2+
GH-19999 (GC refcount assertion failure when destructor modifies variable during concat)
3+
--FILE--
4+
<?php
5+
class Test {
6+
function __destruct() {
7+
global $a;
8+
$a = null;
9+
}
10+
}
11+
12+
$a = [new Test];
13+
$a .= $a;
14+
?>
15+
--EXPECTF--
16+
Warning: Array to string conversion in %s on line %d

Zend/zend_operators.c

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1954,9 +1954,21 @@ ZEND_API zend_result ZEND_FASTCALL shift_right_function(zval *result, zval *op1,
19541954
ZEND_API zend_result ZEND_FASTCALL concat_function(zval *result, zval *op1, zval *op2) /* {{{ */
19551955
{
19561956
zval *orig_op1 = op1;
1957+
zval orig_result = {0};
19571958
zend_string *op1_string, *op2_string;
19581959
bool free_op1_string = false;
19591960
bool free_op2_string = false;
1961+
bool result_is_orig = (result == orig_op1);
1962+
1963+
/* Save the original result value and protect it from being freed during conversion.
1964+
* We add a reference because destructors can modify the zval in-place, and we need
1965+
* to ensure we can properly clean up the original value. */
1966+
if (result_is_orig) {
1967+
ZVAL_COPY_VALUE(&orig_result, result);
1968+
if (Z_REFCOUNTED(orig_result)) {
1969+
Z_ADDREF(orig_result);
1970+
}
1971+
}
19601972

19611973
do {
19621974
if (EXPECTED(Z_TYPE_P(op1) == IS_STRING)) {
@@ -1973,6 +1985,9 @@ ZEND_API zend_result ZEND_FASTCALL concat_function(zval *result, zval *op1, zval
19731985
op1_string = zval_get_string_func(op1);
19741986
if (UNEXPECTED(EG(exception))) {
19751987
zend_string_release(op1_string);
1988+
if (result_is_orig && Z_REFCOUNTED(orig_result)) {
1989+
i_zval_ptr_dtor(&orig_result);
1990+
}
19761991
if (orig_op1 != result) {
19771992
ZVAL_UNDEF(result);
19781993
}
@@ -2008,6 +2023,9 @@ ZEND_API zend_result ZEND_FASTCALL concat_function(zval *result, zval *op1, zval
20082023
if (UNEXPECTED(EG(exception))) {
20092024
zend_string_release(op1_string);
20102025
zend_string_release(op2_string);
2026+
if (result_is_orig && Z_REFCOUNTED(orig_result)) {
2027+
i_zval_ptr_dtor(&orig_result);
2028+
}
20112029
if (orig_op1 != result) {
20122030
ZVAL_UNDEF(result);
20132031
}
@@ -2020,9 +2038,6 @@ ZEND_API zend_result ZEND_FASTCALL concat_function(zval *result, zval *op1, zval
20202038
has_op2_string:;
20212039
if (UNEXPECTED(ZSTR_LEN(op1_string) == 0)) {
20222040
if (EXPECTED(result != op2 || Z_TYPE_P(result) != IS_STRING)) {
2023-
if (result == orig_op1) {
2024-
i_zval_ptr_dtor(result);
2025-
}
20262041
if (free_op2_string) {
20272042
/* transfer ownership of op2_string */
20282043
ZVAL_STR(result, op2_string);
@@ -2033,9 +2048,6 @@ has_op2_string:;
20332048
}
20342049
} else if (UNEXPECTED(ZSTR_LEN(op2_string) == 0)) {
20352050
if (EXPECTED(result != op1 || Z_TYPE_P(result) != IS_STRING)) {
2036-
if (result == orig_op1) {
2037-
i_zval_ptr_dtor(result);
2038-
}
20392051
if (free_op1_string) {
20402052
/* transfer ownership of op1_string */
20412053
ZVAL_STR(result, op1_string);
@@ -2054,6 +2066,9 @@ has_op2_string:;
20542066
if (UNEXPECTED(op1_len > ZSTR_MAX_LEN - op2_len)) {
20552067
if (free_op1_string) zend_string_release(op1_string);
20562068
if (free_op2_string) zend_string_release(op2_string);
2069+
if (result_is_orig && Z_REFCOUNTED(orig_result)) {
2070+
i_zval_ptr_dtor(&orig_result);
2071+
}
20572072
zend_throw_error(NULL, "String size overflow");
20582073
if (orig_op1 != result) {
20592074
ZVAL_UNDEF(result);
@@ -2065,7 +2080,9 @@ has_op2_string:;
20652080
/* Destroy the old result first to drop the refcount, such that $x .= ...; may happen in-place. */
20662081
if (free_op1_string) {
20672082
/* op1_string will be used as the result, so we should not free it */
2068-
i_zval_ptr_dtor(result);
2083+
if (!result_is_orig) {
2084+
i_zval_ptr_dtor(result);
2085+
}
20692086
/* Set it to NULL in case that the extension will throw an out-of-memory error.
20702087
* Otherwise the shutdown sequence will try to free this again. */
20712088
ZVAL_NULL(result);
@@ -2095,6 +2112,10 @@ has_op2_string:;
20952112
ZSTR_VAL(result_str)[result_len] = '\0';
20962113
}
20972114

2115+
if (result_is_orig && Z_REFCOUNTED(orig_result)) {
2116+
i_zval_ptr_dtor(&orig_result);
2117+
}
2118+
20982119
if (free_op1_string) zend_string_release(op1_string);
20992120
if (free_op2_string) zend_string_release(op2_string);
21002121

0 commit comments

Comments
 (0)