1
0
mirror of https://github.com/php/php-src.git synced 2026-03-24 00:02:20 +01:00

Merge branch 'PHP-8.5'

* PHP-8.5:
  Split the live-ranges of loop variables again (#20865)
This commit is contained in:
Bob Weinand
2026-01-15 16:17:34 +01:00
13 changed files with 197 additions and 100 deletions

View File

@@ -136,6 +136,10 @@ static void zend_dump_unused_op(const zend_op *opline, znode_op op, uint32_t fla
if (op.num != (uint32_t)-1) {
fprintf(stderr, " try-catch(%u)", op.num);
}
} else if (ZEND_VM_OP_LOOP_END == (flags & ZEND_VM_OP_MASK)) {
if (opline->extended_value & ZEND_FREE_ON_RETURN) {
fprintf(stderr, " loop-end(+%u)", op.num);
}
} else if (ZEND_VM_OP_THIS == (flags & ZEND_VM_OP_MASK)) {
fprintf(stderr, " THIS");
} else if (ZEND_VM_OP_NEXT == (flags & ZEND_VM_OP_MASK)) {

View File

@@ -1,37 +1,40 @@
--TEST--
GC 050: Destructor are never called twice
GC 050: Try/finally in foreach should create separate live ranges
--FILE--
<?php
class G
{
public static $v;
}
class WithDestructor
{
public function __destruct()
{
echo "d\n";
G::$v = $this;
function f(int $n): object {
try {
foreach ((array) $n as $v) {
if ($n === 1) {
try {
$a = new stdClass;
return $a;
} finally {
return $ret = $a;
}
}
if ($n === 2) {
$b = new stdClass;
return $ret = $b;
}
}
} finally {
$ret->v = 1;
}
return new stdClass;
}
$o = new WithDestructor();
$weakO = \WeakReference::create($o);
echo "---\n";
unset($o);
echo "---\n";
var_dump($weakO->get() !== null); // verify if kept allocated
G::$v = null;
echo "---\n";
var_dump($weakO->get() !== null); // verify if released
for ($i = 0; $i < 100000; $i++) {
// Create cyclic garbage to trigger GC
$a = new stdClass;
$b = new stdClass;
$a->r = $b;
$b->r = $a;
$r = f($i % 2 + 1);
}
echo "OK\n";
?>
--EXPECT--
---
d
---
bool(true)
---
bool(false)
OK

29
Zend/tests/gc_051.phpt Normal file
View File

@@ -0,0 +1,29 @@
--TEST--
GC 048: FE_FREE should mark variable as UNDEF to prevent use-after-free during GC
--FILE--
<?php
// FE_FREE frees the iterator but doesn't set zval to UNDEF
// When GC runs during RETURN, zend_gc_remove_root_tmpvars() may access freed memory
function test_foreach_early_return(string $s): object {
foreach ((array) $s as $v) {
$obj = new stdClass;
// in the early return, the VAR for the cast result is still live
return $obj; // the return may trigger GC
}
}
for ($i = 0; $i < 100000; $i++) {
// create cyclic garbage to fill GC buffer
$a = new stdClass;
$b = new stdClass;
$a->ref = $b;
$b->ref = $a;
$result = test_foreach_early_return("x");
}
echo "OK\n";
?>
--EXPECT--
OK

36
Zend/tests/gc_052.phpt Normal file
View File

@@ -0,0 +1,36 @@
--TEST--
GC 049: Multiple early returns from foreach should create separate live ranges
--FILE--
<?php
function f(int $n): object {
foreach ((array) $n as $v) {
if ($n === 1) {
$a = new stdClass;
return $a;
}
if ($n === 2) {
$b = new stdClass;
return $b;
}
if ($n === 3) {
$c = new stdClass;
return $c;
}
}
return new stdClass;
}
for ($i = 0; $i < 100000; $i++) {
// Create cyclic garbage to trigger GC
$a = new stdClass;
$b = new stdClass;
$a->r = $b;
$b->r = $a;
$r = f($i % 3 + 1);
}
echo "OK\n";
?>
--EXPECT--
OK

37
Zend/tests/gc_053.phpt Normal file
View File

@@ -0,0 +1,37 @@
--TEST--
GC 050: Destructor are never called twice
--FILE--
<?php
class G
{
public static $v;
}
class WithDestructor
{
public function __destruct()
{
echo "d\n";
G::$v = $this;
}
}
$o = new WithDestructor();
$weakO = \WeakReference::create($o);
echo "---\n";
unset($o);
echo "---\n";
var_dump($weakO->get() !== null); // verify if kept allocated
G::$v = null;
echo "---\n";
var_dump($weakO->get() !== null); // verify if released
?>
--EXPECT--
---
d
---
bool(true)
---
bool(false)

View File

@@ -4881,20 +4881,6 @@ static void cleanup_unfinished_calls(zend_execute_data *execute_data, uint32_t o
}
/* }}} */
static const zend_live_range *find_live_range(const zend_op_array *op_array, uint32_t op_num, uint32_t var_num) /* {{{ */
{
int i;
for (i = 0; i < op_array->last_live_range; i++) {
const zend_live_range *range = &op_array->live_range[i];
if (op_num >= range->start && op_num < range->end
&& var_num == (range->var & ~ZEND_LIVE_MASK)) {
return range;
}
}
return NULL;
}
/* }}} */
static void cleanup_live_vars(zend_execute_data *execute_data, uint32_t op_num, uint32_t catch_op_num) /* {{{ */
{
int i;
@@ -4910,6 +4896,16 @@ static void cleanup_live_vars(zend_execute_data *execute_data, uint32_t op_num,
uint32_t var_num = range->var & ~ZEND_LIVE_MASK;
zval *var = EX_VAR(var_num);
/* Handle the split range for loop vars */
if (catch_op_num) {
zend_op *final_op = EX(func)->op_array.opcodes + range->end;
if (final_op->extended_value & ZEND_FREE_ON_RETURN && (final_op->opcode == ZEND_FE_FREE || final_op->opcode == ZEND_FREE)) {
if (catch_op_num < range->end + final_op->op2.num) {
continue;
}
}
}
if (kind == ZEND_LIVE_TMPVAR) {
zval_ptr_dtor_nogc(var);
} else if (kind == ZEND_LIVE_NEW) {

View File

@@ -994,6 +994,35 @@ static void zend_calc_live_ranges(
/* OP_DATA is really part of the previous opcode. */
last_use[var_num] = opnum - (opline->opcode == ZEND_OP_DATA);
}
} else if ((opline->opcode == ZEND_FREE || opline->opcode == ZEND_FE_FREE) && opline->extended_value & ZEND_FREE_ON_RETURN) {
int jump_offset = 1;
while (((opline + jump_offset)->opcode == ZEND_FREE || (opline + jump_offset)->opcode == ZEND_FE_FREE)
&& (opline + jump_offset)->extended_value & ZEND_FREE_ON_RETURN) {
++jump_offset;
}
// loop var frees directly precede the jump (or return) operand, except that ZEND_VERIFY_RETURN_TYPE may happen first.
if ((opline + jump_offset)->opcode == ZEND_VERIFY_RETURN_TYPE) {
++jump_offset;
}
/* FREE with ZEND_FREE_ON_RETURN immediately followed by RETURN frees
* the loop variable on early return. We need to split the live range
* so GC doesn't access the freed variable after this FREE. */
uint32_t opnum_last_use = last_use[var_num];
zend_op *opline_last_use = op_array->opcodes + opnum_last_use;
ZEND_ASSERT(opline_last_use->opcode == opline->opcode); // any ZEND_FREE_ON_RETURN must be followed by a FREE without
if (opnum + jump_offset + 1 != opnum_last_use) {
emit_live_range_raw(op_array, var_num, opline->opcode == ZEND_FE_FREE ? ZEND_LIVE_LOOP : ZEND_LIVE_TMPVAR,
opnum + jump_offset + 1, opnum_last_use);
}
/* Update last_use so next range includes this FREE */
last_use[var_num] = opnum;
/* Store opline offset to loop end */
opline->op2.opline_num = opnum_last_use - opnum;
if (opline_last_use->extended_value & ZEND_FREE_ON_RETURN) {
opline->op2.opline_num += opline_last_use->op2.opline_num;
}
}
}
if (opline->op2_type & (IS_TMP_VAR|IS_VAR)) {

View File

@@ -3246,7 +3246,7 @@ ZEND_VM_COLD_CONST_HANDLER(47, ZEND_JMPNZ_EX, CONST|TMPVAR|CV, JMP_ADDR)
ZEND_VM_JMP(opline);
}
ZEND_VM_HANDLER(70, ZEND_FREE, TMPVAR, ANY)
ZEND_VM_HANDLER(70, ZEND_FREE, TMPVAR, LOOP_END)
{
USE_OPLINE
@@ -3255,7 +3255,7 @@ ZEND_VM_HANDLER(70, ZEND_FREE, TMPVAR, ANY)
ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}
ZEND_VM_HOT_HANDLER(127, ZEND_FE_FREE, TMPVAR, ANY)
ZEND_VM_HOT_HANDLER(127, ZEND_FE_FREE, TMPVAR, LOOP_END)
{
zval *var;
USE_OPLINE
@@ -8185,24 +8185,11 @@ ZEND_VM_HANDLER(149, ZEND_HANDLE_EXCEPTION, ANY, ANY)
&& throw_op->extended_value & ZEND_FREE_ON_RETURN) {
/* exceptions thrown because of loop var destruction on return/break/...
* are logically thrown at the end of the foreach loop, so adjust the
* throw_op_num.
* throw_op_num to the final loop variable FREE.
*/
const zend_live_range *range = find_live_range(
&EX(func)->op_array, throw_op_num, throw_op->op1.var);
/* free op1 of the corresponding RETURN */
for (uint32_t i = throw_op_num; i < range->end; i++) {
if (EX(func)->op_array.opcodes[i].opcode == ZEND_FREE
|| EX(func)->op_array.opcodes[i].opcode == ZEND_FE_FREE) {
/* pass */
} else {
if (EX(func)->op_array.opcodes[i].opcode == ZEND_RETURN
&& (EX(func)->op_array.opcodes[i].op1_type & (IS_VAR|IS_TMP_VAR))) {
zval_ptr_dtor(EX_VAR(EX(func)->op_array.opcodes[i].op1.var));
}
break;
}
}
throw_op_num = range->end;
uint32_t new_throw_op_num = throw_op_num + throw_op->op2.opline_num;
cleanup_live_vars(execute_data, throw_op_num, new_throw_op_num);
throw_op_num = new_throw_op_num;
}
/* Find the innermost try/catch/finally the exception was thrown in */

42
Zend/zend_vm_execute.h generated
View File

@@ -3398,24 +3398,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_HANDLE_EXCEPT
&& throw_op->extended_value & ZEND_FREE_ON_RETURN) {
/* exceptions thrown because of loop var destruction on return/break/...
* are logically thrown at the end of the foreach loop, so adjust the
* throw_op_num.
* throw_op_num to the final loop variable FREE.
*/
const zend_live_range *range = find_live_range(
&EX(func)->op_array, throw_op_num, throw_op->op1.var);
/* free op1 of the corresponding RETURN */
for (uint32_t i = throw_op_num; i < range->end; i++) {
if (EX(func)->op_array.opcodes[i].opcode == ZEND_FREE
|| EX(func)->op_array.opcodes[i].opcode == ZEND_FE_FREE) {
/* pass */
} else {
if (EX(func)->op_array.opcodes[i].opcode == ZEND_RETURN
&& (EX(func)->op_array.opcodes[i].op1_type & (IS_VAR|IS_TMP_VAR))) {
zval_ptr_dtor(EX_VAR(EX(func)->op_array.opcodes[i].op1.var));
}
break;
}
}
throw_op_num = range->end;
uint32_t new_throw_op_num = throw_op_num + throw_op->op2.opline_num;
cleanup_live_vars(execute_data, throw_op_num, new_throw_op_num);
throw_op_num = new_throw_op_num;
}
/* Find the innermost try/catch/finally the exception was thrown in */
@@ -59066,24 +59053,11 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_HANDLE_EXCEPTION_S
&& throw_op->extended_value & ZEND_FREE_ON_RETURN) {
/* exceptions thrown because of loop var destruction on return/break/...
* are logically thrown at the end of the foreach loop, so adjust the
* throw_op_num.
* throw_op_num to the final loop variable FREE.
*/
const zend_live_range *range = find_live_range(
&EX(func)->op_array, throw_op_num, throw_op->op1.var);
/* free op1 of the corresponding RETURN */
for (uint32_t i = throw_op_num; i < range->end; i++) {
if (EX(func)->op_array.opcodes[i].opcode == ZEND_FREE
|| EX(func)->op_array.opcodes[i].opcode == ZEND_FE_FREE) {
/* pass */
} else {
if (EX(func)->op_array.opcodes[i].opcode == ZEND_RETURN
&& (EX(func)->op_array.opcodes[i].op1_type & (IS_VAR|IS_TMP_VAR))) {
zval_ptr_dtor(EX_VAR(EX(func)->op_array.opcodes[i].op1.var));
}
break;
}
}
throw_op_num = range->end;
uint32_t new_throw_op_num = throw_op_num + throw_op->op2.opline_num;
cleanup_live_vars(execute_data, throw_op_num, new_throw_op_num);
throw_op_num = new_throw_op_num;
}
/* Find the innermost try/catch/finally the exception was thrown in */

View File

@@ -64,7 +64,7 @@ $vm_op_flags = array(
"ZEND_VM_OP_NUM" => 0x10,
"ZEND_VM_OP_JMP_ADDR" => 0x20,
"ZEND_VM_OP_TRY_CATCH" => 0x30,
// unused 0x40
"ZEND_VM_OP_LOOP_END" => 0x40,
"ZEND_VM_OP_THIS" => 0x50,
"ZEND_VM_OP_NEXT" => 0x60,
"ZEND_VM_OP_CLASS_FETCH" => 0x70,
@@ -112,6 +112,7 @@ $vm_op_decode = array(
"NUM" => ZEND_VM_OP_NUM,
"JMP_ADDR" => ZEND_VM_OP_JMP_ADDR,
"TRY_CATCH" => ZEND_VM_OP_TRY_CATCH,
"LOOP_END" => ZEND_VM_OP_LOOP_END,
"THIS" => ZEND_VM_OP_THIS,
"NEXT" => ZEND_VM_OP_NEXT,
"CLASS_FETCH" => ZEND_VM_OP_CLASS_FETCH,

View File

@@ -307,7 +307,7 @@ static uint32_t zend_vm_opcodes_flags[211] = {
0x00001301,
0x0100a173,
0x01040300,
0x00000005,
0x00004005,
0x00186703,
0x00106703,
0x08000007,
@@ -364,7 +364,7 @@ static uint32_t zend_vm_opcodes_flags[211] = {
0x00000103,
0x00002003,
0x03000001,
0x00000005,
0x00004005,
0x01000700,
0x00000000,
0x00000000,

View File

@@ -86,6 +86,7 @@ typedef const void* zend_vm_opcode_handler_t;
#define ZEND_VM_OP_NUM 0x00000010
#define ZEND_VM_OP_JMP_ADDR 0x00000020
#define ZEND_VM_OP_TRY_CATCH 0x00000030
#define ZEND_VM_OP_LOOP_END 0x00000040
#define ZEND_VM_OP_THIS 0x00000050
#define ZEND_VM_OP_NEXT 0x00000060
#define ZEND_VM_OP_CLASS_FETCH 0x00000070

View File

@@ -28,11 +28,11 @@ $_main:
0000 T1 = PRE_INC_STATIC_PROP string("prop") string("X")
0001 T2 = ISSET_ISEMPTY_CV (empty) CV0($xx)
0002 JMPZ T2 0005
0003 FREE T1
0003 FREE T1 loop-end(+2)
0004 RETURN null
0005 FREE T1
0006 RETURN int(1)
LIVE RANGES:
1: 0001 - 0005 (tmp/var)
1: 0001 - 0003 (tmp/var)
Deprecated: Increment on non-numeric string is deprecated, use str_increment() instead in %s on line %d