Skip to content

Commit fade10b

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

File tree

2 files changed

+49
-6
lines changed

2 files changed

+49
-6
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: 33 additions & 6 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;
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,8 +2038,8 @@ 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);
2041+
if (result_is_orig && Z_REFCOUNTED(orig_result)) {
2042+
i_zval_ptr_dtor(&orig_result);
20252043
}
20262044
if (free_op2_string) {
20272045
/* transfer ownership of op2_string */
@@ -2033,8 +2051,8 @@ has_op2_string:;
20332051
}
20342052
} else if (UNEXPECTED(ZSTR_LEN(op2_string) == 0)) {
20352053
if (EXPECTED(result != op1 || Z_TYPE_P(result) != IS_STRING)) {
2036-
if (result == orig_op1) {
2037-
i_zval_ptr_dtor(result);
2054+
if (result_is_orig && Z_REFCOUNTED(orig_result)) {
2055+
i_zval_ptr_dtor(&orig_result);
20382056
}
20392057
if (free_op1_string) {
20402058
/* transfer ownership of op1_string */
@@ -2054,6 +2072,9 @@ has_op2_string:;
20542072
if (UNEXPECTED(op1_len > ZSTR_MAX_LEN - op2_len)) {
20552073
if (free_op1_string) zend_string_release(op1_string);
20562074
if (free_op2_string) zend_string_release(op2_string);
2075+
if (result_is_orig && Z_REFCOUNTED(orig_result)) {
2076+
i_zval_ptr_dtor(&orig_result);
2077+
}
20572078
zend_throw_error(NULL, "String size overflow");
20582079
if (orig_op1 != result) {
20592080
ZVAL_UNDEF(result);
@@ -2065,7 +2086,11 @@ has_op2_string:;
20652086
/* Destroy the old result first to drop the refcount, such that $x .= ...; may happen in-place. */
20662087
if (free_op1_string) {
20672088
/* op1_string will be used as the result, so we should not free it */
2068-
i_zval_ptr_dtor(result);
2089+
if (result_is_orig && Z_REFCOUNTED(orig_result)) {
2090+
i_zval_ptr_dtor(&orig_result);
2091+
} else if (!result_is_orig) {
2092+
i_zval_ptr_dtor(result);
2093+
}
20692094
/* Set it to NULL in case that the extension will throw an out-of-memory error.
20702095
* Otherwise the shutdown sequence will try to free this again. */
20712096
ZVAL_NULL(result);
@@ -2084,7 +2109,9 @@ has_op2_string:;
20842109
} else {
20852110
result_str = zend_string_alloc(result_len, 0);
20862111
memcpy(ZSTR_VAL(result_str), ZSTR_VAL(op1_string), op1_len);
2087-
if (result == orig_op1) {
2112+
if (result_is_orig && Z_REFCOUNTED(orig_result)) {
2113+
i_zval_ptr_dtor(&orig_result);
2114+
} else if (result == orig_op1) {
20882115
i_zval_ptr_dtor(result);
20892116
}
20902117
}

0 commit comments

Comments
 (0)