Skip to content

Commit 163b9ef

Browse files
committed
Fix GH-21639: Protect frameless args during __toString reentry
1 parent f7eb5ef commit 163b9ef

File tree

10 files changed

+288
-0
lines changed

10 files changed

+288
-0
lines changed

Zend/tests/gh21639.phpt

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
--TEST--
2+
GH-21639: Frameless calls keep volatile CV arguments alive during __toString()
3+
--FILE--
4+
<?php
5+
class ImplodeElement {
6+
public function __toString(): string {
7+
global $separator, $pieces;
8+
9+
$separator = null;
10+
$pieces = null;
11+
12+
return "C";
13+
}
14+
}
15+
16+
$separator = str_repeat(",", 1) . " ";
17+
$pieces = [new ImplodeElement(), 42];
18+
19+
var_dump(implode($separator, $pieces));
20+
var_dump($separator, $pieces);
21+
22+
class ImplodeElementWithoutSeparator {
23+
public function __toString(): string {
24+
global $oneArgPieces;
25+
26+
$oneArgPieces = null;
27+
28+
return "D";
29+
}
30+
}
31+
32+
$oneArgPieces = [new ImplodeElementWithoutSeparator(), 42];
33+
34+
var_dump(implode($oneArgPieces));
35+
var_dump($oneArgPieces);
36+
37+
class InArrayNeedle {
38+
public function __toString(): string {
39+
global $inArrayHaystack;
40+
41+
$inArrayHaystack = null;
42+
43+
return "needle";
44+
}
45+
}
46+
47+
$inArrayHaystack = [new InArrayNeedle()];
48+
49+
var_dump(in_array("needle", $inArrayHaystack));
50+
var_dump($inArrayHaystack);
51+
52+
class StrtrReplacement {
53+
public function __toString(): string {
54+
global $strtrReplacements;
55+
56+
$strtrReplacements = null;
57+
58+
return "b";
59+
}
60+
}
61+
62+
$strtrReplacements = ["a" => new StrtrReplacement()];
63+
64+
var_dump(strtr("a", $strtrReplacements));
65+
var_dump($strtrReplacements);
66+
67+
class StrReplaceSubject {
68+
public function __toString(): string {
69+
global $strReplaceSubject;
70+
71+
$strReplaceSubject = null;
72+
73+
return "a";
74+
}
75+
}
76+
77+
$strReplaceSubject = [new StrReplaceSubject(), "aa"];
78+
79+
var_dump(str_replace("a", "b", $strReplaceSubject));
80+
var_dump($strReplaceSubject);
81+
?>
82+
--EXPECT--
83+
string(5) "C, 42"
84+
NULL
85+
NULL
86+
string(3) "D42"
87+
NULL
88+
bool(true)
89+
NULL
90+
string(1) "b"
91+
NULL
92+
array(2) {
93+
[0]=>
94+
string(1) "b"
95+
[1]=>
96+
string(2) "bb"
97+
}
98+
NULL

Zend/zend.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,7 @@ static void executor_globals_ctor(zend_executor_globals *executor_globals) /* {{
810810
ZVAL_UNDEF(&executor_globals->user_exception_handler);
811811
executor_globals->in_autoload = NULL;
812812
executor_globals->current_execute_data = NULL;
813+
executor_globals->frameless_reentry_copies = NULL;
813814
executor_globals->current_module = NULL;
814815
executor_globals->exit_status = 0;
815816
#if XPFPA_HAVE_CW

Zend/zend_execute.c

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1582,6 +1582,149 @@ static zend_never_inline void zend_assign_to_object_dim(zend_object *obj, zval *
15821582
}
15831583
}
15841584

1585+
struct _zend_frameless_reentry_copies {
1586+
struct _zend_frameless_reentry_copies *prev;
1587+
zend_execute_data *execute_data;
1588+
const zend_op *opline;
1589+
uint8_t copied_args;
1590+
zval args[3];
1591+
};
1592+
1593+
static zend_always_inline bool zend_frameless_arg_needs_reentry_copy(zval *zv)
1594+
{
1595+
ZVAL_DEREF(zv);
1596+
return Z_TYPE_P(zv) == IS_ARRAY || Z_TYPE_P(zv) == IS_STRING;
1597+
}
1598+
1599+
static void zend_frameless_reentry_copy_arg(zend_frameless_reentry_copies *copies, uint32_t arg, zval *zv)
1600+
{
1601+
if (!zend_frameless_arg_needs_reentry_copy(zv)) {
1602+
return;
1603+
}
1604+
1605+
ZVAL_COPY_DEREF(&copies->args[arg], zv);
1606+
copies->copied_args |= (1u << arg);
1607+
}
1608+
1609+
static bool zend_frameless_reentry_has_copies(zend_execute_data *execute_data, const zend_op *opline)
1610+
{
1611+
for (zend_frameless_reentry_copies *copies = EG(frameless_reentry_copies);
1612+
copies;
1613+
copies = copies->prev) {
1614+
if (copies->execute_data == execute_data && copies->opline == opline) {
1615+
return true;
1616+
}
1617+
}
1618+
1619+
return false;
1620+
}
1621+
1622+
ZEND_API bool zend_frameless_protect_args_for_reentry(void)
1623+
{
1624+
zend_execute_data *execute_data = EG(current_execute_data);
1625+
if (!execute_data) {
1626+
return false;
1627+
}
1628+
1629+
const zend_op *opline = EX(opline);
1630+
if (!opline || !ZEND_OP_IS_FRAMELESS_ICALL(opline->opcode)) {
1631+
return false;
1632+
}
1633+
1634+
if (zend_frameless_reentry_has_copies(execute_data, opline)) {
1635+
return true;
1636+
}
1637+
1638+
uint8_t num_args = ZEND_FLF_NUM_ARGS(opline->opcode);
1639+
if (num_args == 0) {
1640+
return false;
1641+
}
1642+
1643+
zend_frameless_reentry_copies *copies = emalloc(sizeof(zend_frameless_reentry_copies));
1644+
copies->execute_data = execute_data;
1645+
copies->opline = opline;
1646+
copies->copied_args = 0;
1647+
1648+
if (opline->op1_type == IS_CV) {
1649+
zend_frameless_reentry_copy_arg(copies, 0,
1650+
zend_get_zval_ptr(opline, opline->op1_type, &opline->op1, execute_data));
1651+
}
1652+
if (num_args >= 2 && opline->op2_type == IS_CV) {
1653+
zend_frameless_reentry_copy_arg(copies, 1,
1654+
zend_get_zval_ptr(opline, opline->op2_type, &opline->op2, execute_data));
1655+
}
1656+
if (num_args >= 3 && (opline + 1)->op1_type == IS_CV) {
1657+
zend_frameless_reentry_copy_arg(copies, 2,
1658+
zend_get_zval_ptr(opline + 1, (opline + 1)->op1_type, &(opline + 1)->op1, execute_data));
1659+
}
1660+
1661+
if (copies->copied_args == 0) {
1662+
efree(copies);
1663+
return false;
1664+
}
1665+
1666+
copies->prev = EG(frameless_reentry_copies);
1667+
EG(frameless_reentry_copies) = copies;
1668+
1669+
return true;
1670+
}
1671+
1672+
ZEND_API void zend_frameless_schedule_reentry_cleanup(void)
1673+
{
1674+
zend_atomic_bool_store_ex(&EG(vm_interrupt), true);
1675+
}
1676+
1677+
static bool zend_frameless_reentry_copies_in_use(zend_frameless_reentry_copies *copies)
1678+
{
1679+
for (zend_execute_data *execute_data = EG(current_execute_data);
1680+
execute_data;
1681+
execute_data = execute_data->prev_execute_data) {
1682+
if (execute_data == copies->execute_data && execute_data->opline == copies->opline) {
1683+
return true;
1684+
}
1685+
}
1686+
1687+
return false;
1688+
}
1689+
1690+
static void zend_frameless_cleanup_reentry_copies_ex(bool force)
1691+
{
1692+
zend_frameless_reentry_copies **next = &EG(frameless_reentry_copies);
1693+
1694+
while (*next) {
1695+
zend_frameless_reentry_copies *copies = *next;
1696+
1697+
if (!force && zend_frameless_reentry_copies_in_use(copies)) {
1698+
next = &copies->prev;
1699+
continue;
1700+
}
1701+
1702+
*next = copies->prev;
1703+
1704+
for (uint32_t i = 0; i < 3; i++) {
1705+
if (copies->copied_args & (1u << i)) {
1706+
zval_ptr_dtor(&copies->args[i]);
1707+
}
1708+
}
1709+
1710+
efree(copies);
1711+
}
1712+
1713+
if (EG(frameless_reentry_copies)) {
1714+
zend_atomic_bool_store_ex(&EG(vm_interrupt), true);
1715+
}
1716+
}
1717+
1718+
ZEND_API void zend_frameless_cleanup_reentry_copies(void)
1719+
{
1720+
zend_frameless_cleanup_reentry_copies_ex(false);
1721+
}
1722+
1723+
ZEND_API void zend_frameless_cleanup_reentry_copies_force(void)
1724+
{
1725+
zend_frameless_cleanup_reentry_copies_ex(true);
1726+
}
1727+
15851728
static void frameless_observed_call_copy(zend_execute_data *call, uint32_t arg, zval *zv)
15861729
{
15871730
if (Z_ISUNDEF_P(zv)) {
@@ -4081,6 +4224,7 @@ ZEND_API void ZEND_FASTCALL zend_free_compiled_variables(zend_execute_data *exec
40814224
ZEND_API ZEND_COLD void ZEND_FASTCALL zend_fcall_interrupt(zend_execute_data *call)
40824225
{
40834226
zend_atomic_bool_store_ex(&EG(vm_interrupt), false);
4227+
zend_frameless_cleanup_reentry_copies();
40844228
if (zend_atomic_bool_load_ex(&EG(timed_out))) {
40854229
zend_timeout();
40864230
} else if (zend_interrupt_function) {

Zend/zend_execute.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,10 @@ ZEND_API zend_result zend_set_user_opcode_handler(uint8_t opcode, user_opcode_ha
428428
ZEND_API user_opcode_handler_t zend_get_user_opcode_handler(uint8_t opcode);
429429

430430
ZEND_API zval *zend_get_zval_ptr(const zend_op *opline, int op_type, const znode_op *node, const zend_execute_data *execute_data);
431+
ZEND_API bool zend_frameless_protect_args_for_reentry(void);
432+
ZEND_API void zend_frameless_schedule_reentry_cleanup(void);
433+
ZEND_API void zend_frameless_cleanup_reentry_copies(void);
434+
ZEND_API void zend_frameless_cleanup_reentry_copies_force(void);
431435

432436
ZEND_API void zend_clean_and_cache_symbol_table(zend_array *symbol_table);
433437
ZEND_API void ZEND_FASTCALL zend_free_compiled_variables(zend_execute_data *execute_data);

Zend/zend_execute_API.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ void init_executor(void) /* {{{ */
176176
EG(full_tables_cleanup) = 0;
177177
ZEND_ATOMIC_BOOL_INIT(&EG(vm_interrupt), false);
178178
ZEND_ATOMIC_BOOL_INIT(&EG(timed_out), false);
179+
EG(frameless_reentry_copies) = NULL;
179180

180181
EG(exception) = NULL;
181182
EG(prev_exception) = NULL;
@@ -275,6 +276,8 @@ ZEND_API void zend_shutdown_executor_values(bool fast_shutdown)
275276
zend_string *key;
276277
zval *zv;
277278

279+
zend_frameless_cleanup_reentry_copies_force();
280+
278281
EG(flags) |= EG_FLAGS_IN_RESOURCE_SHUTDOWN;
279282
zend_close_rsrc_list(&EG(regular_list));
280283

@@ -1039,6 +1042,7 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
10391042
/* This flag is regularly checked while running user functions, but not internal
10401043
* So see whether interrupt flag was set while the function was running... */
10411044
if (zend_atomic_bool_exchange_ex(&EG(vm_interrupt), false)) {
1045+
zend_frameless_cleanup_reentry_copies();
10421046
if (zend_atomic_bool_load_ex(&EG(timed_out))) {
10431047
zend_timeout();
10441048
} else if (zend_interrupt_function) {

Zend/zend_globals.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ typedef struct _zend_vm_stack *zend_vm_stack;
7474
typedef struct _zend_ini_entry zend_ini_entry;
7575
typedef struct _zend_fiber_context zend_fiber_context;
7676
typedef struct _zend_fiber zend_fiber;
77+
typedef struct _zend_frameless_reentry_copies zend_frameless_reentry_copies;
7778

7879
typedef enum {
7980
ZEND_MEMOIZE_NONE,
@@ -215,6 +216,7 @@ struct _zend_executor_globals {
215216

216217
zend_atomic_bool vm_interrupt;
217218
zend_atomic_bool timed_out;
219+
zend_frameless_reentry_copies *frameless_reentry_copies;
218220

219221
HashTable *in_autoload;
220222

Zend/zend_object_handlers.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2558,9 +2558,13 @@ ZEND_API zend_result zend_std_cast_object_tostring(zend_object *readobj, zval *w
25582558
zend_class_entry *ce = readobj->ce;
25592559
if (ce->__tostring) {
25602560
zval retval;
2561+
bool frameless_reentry = zend_frameless_protect_args_for_reentry();
25612562
GC_ADDREF(readobj);
25622563
zend_call_known_instance_method_with_0_params(ce->__tostring, readobj, &retval);
25632564
zend_object_release(readobj);
2565+
if (frameless_reentry) {
2566+
zend_frameless_schedule_reentry_cleanup();
2567+
}
25642568
if (EXPECTED(Z_TYPE(retval) == IS_STRING)) {
25652569
ZVAL_COPY_VALUE(writeobj, &retval);
25662570
return SUCCESS;

Zend/zend_vm_def.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10410,6 +10410,7 @@ ZEND_VM_DEFINE_OP(137, ZEND_OP_DATA);
1041010410
ZEND_VM_HELPER(zend_interrupt_helper, ANY, ANY)
1041110411
{
1041210412
zend_atomic_bool_store_ex(&EG(vm_interrupt), false);
10413+
zend_frameless_cleanup_reentry_copies();
1041310414
SAVE_OPLINE();
1041410415
if (zend_atomic_bool_load_ex(&EG(timed_out))) {
1041510416
zend_timeout();

Zend/zend_vm_execute.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/pcre/tests/gh21639.phpt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
GH-21639: Frameless preg_replace keeps volatile CV arguments alive during __toString()
3+
--EXTENSIONS--
4+
pcre
5+
--FILE--
6+
<?php
7+
class PregReplaceSubject {
8+
public function __toString(): string {
9+
global $pregReplaceSubject;
10+
11+
$pregReplaceSubject = null;
12+
13+
return "a";
14+
}
15+
}
16+
17+
$pregReplaceSubject = [new PregReplaceSubject(), "aa"];
18+
19+
var_dump(preg_replace("/a/", "b", $pregReplaceSubject));
20+
var_dump($pregReplaceSubject);
21+
?>
22+
--EXPECT--
23+
array(2) {
24+
[0]=>
25+
string(1) "b"
26+
[1]=>
27+
string(2) "bb"
28+
}
29+
NULL

0 commit comments

Comments
 (0)