Skip to content

Commit b95d2ee

Browse files
committed
Merge branch 'PHP-8.5'
* PHP-8.5: Split the live-ranges of loop variables again (php#20865)
2 parents d136b21 + c878380 commit b95d2ee

File tree

13 files changed

+196
-99
lines changed

13 files changed

+196
-99
lines changed

Zend/Optimizer/zend_dump.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ static void zend_dump_unused_op(const zend_op *opline, znode_op op, uint32_t fla
136136
if (op.num != (uint32_t)-1) {
137137
fprintf(stderr, " try-catch(%u)", op.num);
138138
}
139+
} else if (ZEND_VM_OP_LOOP_END == (flags & ZEND_VM_OP_MASK)) {
140+
if (opline->extended_value & ZEND_FREE_ON_RETURN) {
141+
fprintf(stderr, " loop-end(+%u)", op.num);
142+
}
139143
} else if (ZEND_VM_OP_THIS == (flags & ZEND_VM_OP_MASK)) {
140144
fprintf(stderr, " THIS");
141145
} else if (ZEND_VM_OP_NEXT == (flags & ZEND_VM_OP_MASK)) {

Zend/tests/gc/gc_050.phpt

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,40 @@
11
--TEST--
2-
GC 050: Destructor are never called twice
2+
GC 050: Try/finally in foreach should create separate live ranges
33
--FILE--
44
<?php
55

6-
class G
7-
{
8-
public static $v;
6+
function f(int $n): object {
7+
try {
8+
foreach ((array) $n as $v) {
9+
if ($n === 1) {
10+
try {
11+
$a = new stdClass;
12+
return $a;
13+
} finally {
14+
return $ret = $a;
15+
}
16+
}
17+
if ($n === 2) {
18+
$b = new stdClass;
19+
return $ret = $b;
20+
}
21+
}
22+
} finally {
23+
$ret->v = 1;
24+
}
25+
return new stdClass;
926
}
1027

11-
class WithDestructor
12-
{
13-
public function __destruct()
14-
{
15-
echo "d\n";
28+
for ($i = 0; $i < 100000; $i++) {
29+
// Create cyclic garbage to trigger GC
30+
$a = new stdClass;
31+
$b = new stdClass;
32+
$a->r = $b;
33+
$b->r = $a;
1634

17-
G::$v = $this;
18-
}
35+
$r = f($i % 2 + 1);
1936
}
20-
21-
$o = new WithDestructor();
22-
$weakO = \WeakReference::create($o);
23-
echo "---\n";
24-
unset($o);
25-
echo "---\n";
26-
var_dump($weakO->get() !== null); // verify if kept allocated
27-
G::$v = null;
28-
echo "---\n";
29-
var_dump($weakO->get() !== null); // verify if released
37+
echo "OK\n";
3038
?>
3139
--EXPECT--
32-
---
33-
d
34-
---
35-
bool(true)
36-
---
37-
bool(false)
40+
OK

Zend/tests/gc_051.phpt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
GC 048: FE_FREE should mark variable as UNDEF to prevent use-after-free during GC
3+
--FILE--
4+
<?php
5+
// FE_FREE frees the iterator but doesn't set zval to UNDEF
6+
// When GC runs during RETURN, zend_gc_remove_root_tmpvars() may access freed memory
7+
8+
function test_foreach_early_return(string $s): object {
9+
foreach ((array) $s as $v) {
10+
$obj = new stdClass;
11+
// in the early return, the VAR for the cast result is still live
12+
return $obj; // the return may trigger GC
13+
}
14+
}
15+
16+
for ($i = 0; $i < 100000; $i++) {
17+
// create cyclic garbage to fill GC buffer
18+
$a = new stdClass;
19+
$b = new stdClass;
20+
$a->ref = $b;
21+
$b->ref = $a;
22+
23+
$result = test_foreach_early_return("x");
24+
}
25+
26+
echo "OK\n";
27+
?>
28+
--EXPECT--
29+
OK

Zend/tests/gc_052.phpt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
--TEST--
2+
GC 049: Multiple early returns from foreach should create separate live ranges
3+
--FILE--
4+
<?php
5+
6+
function f(int $n): object {
7+
foreach ((array) $n as $v) {
8+
if ($n === 1) {
9+
$a = new stdClass;
10+
return $a;
11+
}
12+
if ($n === 2) {
13+
$b = new stdClass;
14+
return $b;
15+
}
16+
if ($n === 3) {
17+
$c = new stdClass;
18+
return $c;
19+
}
20+
}
21+
return new stdClass;
22+
}
23+
24+
for ($i = 0; $i < 100000; $i++) {
25+
// Create cyclic garbage to trigger GC
26+
$a = new stdClass;
27+
$b = new stdClass;
28+
$a->r = $b;
29+
$b->r = $a;
30+
31+
$r = f($i % 3 + 1);
32+
}
33+
echo "OK\n";
34+
?>
35+
--EXPECT--
36+
OK

Zend/tests/gc_053.phpt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
GC 050: Destructor are never called twice
3+
--FILE--
4+
<?php
5+
6+
class G
7+
{
8+
public static $v;
9+
}
10+
11+
class WithDestructor
12+
{
13+
public function __destruct()
14+
{
15+
echo "d\n";
16+
17+
G::$v = $this;
18+
}
19+
}
20+
21+
$o = new WithDestructor();
22+
$weakO = \WeakReference::create($o);
23+
echo "---\n";
24+
unset($o);
25+
echo "---\n";
26+
var_dump($weakO->get() !== null); // verify if kept allocated
27+
G::$v = null;
28+
echo "---\n";
29+
var_dump($weakO->get() !== null); // verify if released
30+
?>
31+
--EXPECT--
32+
---
33+
d
34+
---
35+
bool(true)
36+
---
37+
bool(false)

Zend/zend_execute.c

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4881,20 +4881,6 @@ static void cleanup_unfinished_calls(zend_execute_data *execute_data, uint32_t o
48814881
}
48824882
/* }}} */
48834883

4884-
static const zend_live_range *find_live_range(const zend_op_array *op_array, uint32_t op_num, uint32_t var_num) /* {{{ */
4885-
{
4886-
int i;
4887-
for (i = 0; i < op_array->last_live_range; i++) {
4888-
const zend_live_range *range = &op_array->live_range[i];
4889-
if (op_num >= range->start && op_num < range->end
4890-
&& var_num == (range->var & ~ZEND_LIVE_MASK)) {
4891-
return range;
4892-
}
4893-
}
4894-
return NULL;
4895-
}
4896-
/* }}} */
4897-
48984884
static void cleanup_live_vars(zend_execute_data *execute_data, uint32_t op_num, uint32_t catch_op_num) /* {{{ */
48994885
{
49004886
int i;
@@ -4910,6 +4896,16 @@ static void cleanup_live_vars(zend_execute_data *execute_data, uint32_t op_num,
49104896
uint32_t var_num = range->var & ~ZEND_LIVE_MASK;
49114897
zval *var = EX_VAR(var_num);
49124898

4899+
/* Handle the split range for loop vars */
4900+
if (catch_op_num) {
4901+
zend_op *final_op = EX(func)->op_array.opcodes + range->end;
4902+
if (final_op->extended_value & ZEND_FREE_ON_RETURN && (final_op->opcode == ZEND_FE_FREE || final_op->opcode == ZEND_FREE)) {
4903+
if (catch_op_num < range->end + final_op->op2.num) {
4904+
continue;
4905+
}
4906+
}
4907+
}
4908+
49134909
if (kind == ZEND_LIVE_TMPVAR) {
49144910
zval_ptr_dtor_nogc(var);
49154911
} else if (kind == ZEND_LIVE_NEW) {

Zend/zend_opcode.c

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,35 @@ static void zend_calc_live_ranges(
994994
/* OP_DATA is really part of the previous opcode. */
995995
last_use[var_num] = opnum - (opline->opcode == ZEND_OP_DATA);
996996
}
997+
} else if ((opline->opcode == ZEND_FREE || opline->opcode == ZEND_FE_FREE) && opline->extended_value & ZEND_FREE_ON_RETURN) {
998+
int jump_offset = 1;
999+
while (((opline + jump_offset)->opcode == ZEND_FREE || (opline + jump_offset)->opcode == ZEND_FE_FREE)
1000+
&& (opline + jump_offset)->extended_value & ZEND_FREE_ON_RETURN) {
1001+
++jump_offset;
1002+
}
1003+
// loop var frees directly precede the jump (or return) operand, except that ZEND_VERIFY_RETURN_TYPE may happen first.
1004+
if ((opline + jump_offset)->opcode == ZEND_VERIFY_RETURN_TYPE) {
1005+
++jump_offset;
1006+
}
1007+
/* FREE with ZEND_FREE_ON_RETURN immediately followed by RETURN frees
1008+
* the loop variable on early return. We need to split the live range
1009+
* so GC doesn't access the freed variable after this FREE. */
1010+
uint32_t opnum_last_use = last_use[var_num];
1011+
zend_op *opline_last_use = op_array->opcodes + opnum_last_use;
1012+
ZEND_ASSERT(opline_last_use->opcode == opline->opcode); // any ZEND_FREE_ON_RETURN must be followed by a FREE without
1013+
if (opnum + jump_offset + 1 != opnum_last_use) {
1014+
emit_live_range_raw(op_array, var_num, opline->opcode == ZEND_FE_FREE ? ZEND_LIVE_LOOP : ZEND_LIVE_TMPVAR,
1015+
opnum + jump_offset + 1, opnum_last_use);
1016+
}
1017+
1018+
/* Update last_use so next range includes this FREE */
1019+
last_use[var_num] = opnum;
1020+
1021+
/* Store opline offset to loop end */
1022+
opline->op2.opline_num = opnum_last_use - opnum;
1023+
if (opline_last_use->extended_value & ZEND_FREE_ON_RETURN) {
1024+
opline->op2.opline_num += opline_last_use->op2.opline_num;
1025+
}
9971026
}
9981027
}
9991028
if (opline->op2_type & (IS_TMP_VAR|IS_VAR)) {

Zend/zend_vm_def.h

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3246,7 +3246,7 @@ ZEND_VM_COLD_CONST_HANDLER(47, ZEND_JMPNZ_EX, CONST|TMPVAR|CV, JMP_ADDR)
32463246
ZEND_VM_JMP(opline);
32473247
}
32483248

3249-
ZEND_VM_HANDLER(70, ZEND_FREE, TMPVAR, ANY)
3249+
ZEND_VM_HANDLER(70, ZEND_FREE, TMPVAR, LOOP_END)
32503250
{
32513251
USE_OPLINE
32523252

@@ -3255,7 +3255,7 @@ ZEND_VM_HANDLER(70, ZEND_FREE, TMPVAR, ANY)
32553255
ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
32563256
}
32573257

3258-
ZEND_VM_HOT_HANDLER(127, ZEND_FE_FREE, TMPVAR, ANY)
3258+
ZEND_VM_HOT_HANDLER(127, ZEND_FE_FREE, TMPVAR, LOOP_END)
32593259
{
32603260
zval *var;
32613261
USE_OPLINE
@@ -8185,24 +8185,11 @@ ZEND_VM_HANDLER(149, ZEND_HANDLE_EXCEPTION, ANY, ANY)
81858185
&& throw_op->extended_value & ZEND_FREE_ON_RETURN) {
81868186
/* exceptions thrown because of loop var destruction on return/break/...
81878187
* are logically thrown at the end of the foreach loop, so adjust the
8188-
* throw_op_num.
8188+
* throw_op_num to the final loop variable FREE.
81898189
*/
8190-
const zend_live_range *range = find_live_range(
8191-
&EX(func)->op_array, throw_op_num, throw_op->op1.var);
8192-
/* free op1 of the corresponding RETURN */
8193-
for (uint32_t i = throw_op_num; i < range->end; i++) {
8194-
if (EX(func)->op_array.opcodes[i].opcode == ZEND_FREE
8195-
|| EX(func)->op_array.opcodes[i].opcode == ZEND_FE_FREE) {
8196-
/* pass */
8197-
} else {
8198-
if (EX(func)->op_array.opcodes[i].opcode == ZEND_RETURN
8199-
&& (EX(func)->op_array.opcodes[i].op1_type & (IS_VAR|IS_TMP_VAR))) {
8200-
zval_ptr_dtor(EX_VAR(EX(func)->op_array.opcodes[i].op1.var));
8201-
}
8202-
break;
8203-
}
8204-
}
8205-
throw_op_num = range->end;
8190+
uint32_t new_throw_op_num = throw_op_num + throw_op->op2.opline_num;
8191+
cleanup_live_vars(execute_data, throw_op_num, new_throw_op_num);
8192+
throw_op_num = new_throw_op_num;
82068193
}
82078194

82088195
/* Find the innermost try/catch/finally the exception was thrown in */

Zend/zend_vm_execute.h

Lines changed: 8 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Zend/zend_vm_gen.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"ZEND_VM_OP_NUM" => 0x10,
6565
"ZEND_VM_OP_JMP_ADDR" => 0x20,
6666
"ZEND_VM_OP_TRY_CATCH" => 0x30,
67-
// unused 0x40
67+
"ZEND_VM_OP_LOOP_END" => 0x40,
6868
"ZEND_VM_OP_THIS" => 0x50,
6969
"ZEND_VM_OP_NEXT" => 0x60,
7070
"ZEND_VM_OP_CLASS_FETCH" => 0x70,
@@ -112,6 +112,7 @@
112112
"NUM" => ZEND_VM_OP_NUM,
113113
"JMP_ADDR" => ZEND_VM_OP_JMP_ADDR,
114114
"TRY_CATCH" => ZEND_VM_OP_TRY_CATCH,
115+
"LOOP_END" => ZEND_VM_OP_LOOP_END,
115116
"THIS" => ZEND_VM_OP_THIS,
116117
"NEXT" => ZEND_VM_OP_NEXT,
117118
"CLASS_FETCH" => ZEND_VM_OP_CLASS_FETCH,

0 commit comments

Comments
 (0)