Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Zend/tests/gc_048.phpt
Original file line number Diff line number Diff line change
@@ -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 changes: 36 additions & 0 deletions Zend/tests/gc_049.phpt
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions Zend/zend_opcode.c
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,38 @@ 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_FE_FREE
&& opline->extended_value & ZEND_FREE_ON_RETURN
&& opnum + 1 < op_array->last
&& ((opline + 1)->opcode == ZEND_RETURN
|| (opline + 1)->opcode == ZEND_RETURN_BY_REF
|| (opline + 1)->opcode == ZEND_GENERATOR_RETURN)) {
/* FE_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 FE_FREE.
*
* FE_FREE is included in the range only if it pertains to an early
* return. */
uint32_t opnum_last_use = last_use[var_num]; // likely a FE_FREE
__auto_type opline_last_use = &op_array->opcodes[opnum_last_use];
if (opline_last_use->opcode == ZEND_FE_FREE &&
opline_last_use->extended_value & ZEND_FREE_ON_RETURN) {
/* another early return; we include the FE_FREE */
emit_live_range_raw(op_array, var_num, ZEND_LIVE_LOOP,
opnum + 2, opnum_last_use + 1);
} else if (opline_last_use->opcode == ZEND_FE_FREE &&
!(opline_last_use->extended_value & ZEND_FREE_ON_RETURN)) {
/* the normal return; don't include the FE_FREE */
emit_live_range_raw(op_array, var_num, ZEND_LIVE_LOOP,
opnum + 2, opnum_last_use);
} else {
/* if the last use is not FE_FREE, include it */
emit_live_range_raw(op_array, var_num, ZEND_LIVE_LOOP,
opnum + 2, opnum_last_use + 1);
}

/* Update last_use so next range includes this FE_FREE */
last_use[var_num] = opnum + 1;
}
}
if (opline->op2_type & (IS_TMP_VAR|IS_VAR)) {
Expand Down
40 changes: 33 additions & 7 deletions Zend/zend_vm_def.h
Original file line number Diff line number Diff line change
Expand Up @@ -8059,19 +8059,45 @@ ZEND_VM_HANDLER(149, ZEND_HANDLE_EXCEPTION, ANY, ANY)
*/
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 (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) {

/* free op1 of the corresponding RETURN - must use original throw_op_num
* and first range, before any split-range skipping */
uint32_t range_end = range->end;
for (i = throw_op_num; i < range_end; i++) {
__auto_type current_opline = EX(func)->op_array.opcodes[i];
if (current_opline.opcode == ZEND_FREE
|| current_opline.opcode == ZEND_FE_FREE) {
if (current_opline.extended_value & ZEND_FREE_ON_RETURN) {
/* if this is a split end, the ZEND_RETURN is not included
* in the range, so extend the range */
range_end++;
}
/* 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));
if (current_opline.opcode == ZEND_RETURN
&& (current_opline.op1_type & (IS_VAR|IS_TMP_VAR))) {
zval_ptr_dtor(EX_VAR(current_opline.op1.var));
}
break;
}
}

/* skip any split ranges to find the final range of the loop var and
* adjust throw_op_num */
for (;;) {
if (range->end < EX(func)->op_array.last) {
__auto_type last_range_opline = EX(func)->op_array.opcodes[range->end - 1];
if (last_range_opline.opcode == ZEND_FE_FREE &&
(last_range_opline.extended_value & ZEND_FREE_ON_RETURN)) {
/* the range was split, skip to find the final range */
throw_op_num = range->end + 1;
range = find_live_range(
&EX(func)->op_array, throw_op_num, throw_op->op1.var);
continue;
}
}
break;
}
throw_op_num = range->end;
}

Expand Down
40 changes: 33 additions & 7 deletions Zend/zend_vm_execute.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading