mirror of
https://github.com/php/php-src.git
synced 2026-03-24 00:02:20 +01:00
Merge branch 'PHP-8.4' of github.com:php/php-src into PHP-8.5
* 'PHP-8.4' of github.com:php/php-src: Split the live-ranges of loop variables again (#20865)
This commit is contained in:
1
NEWS
1
NEWS
@@ -15,6 +15,7 @@ PHP NEWS
|
||||
. Fix OSS-Fuzz #474613951 (Leaked parent property default value). (ilutov)
|
||||
. Fixed bug GH-20895 (ReflectionProperty does not return the PHPDoc of a
|
||||
property if it contains an attribute with a Closure). (timwolla)
|
||||
. Fixed bug GH-20766 (Use-after-free in FE_FREE with GC interaction). (Bob)
|
||||
|
||||
- Date:
|
||||
. Update timelib to 2022.16. (Derick)
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
29
Zend/tests/gc_051.phpt
Normal 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
36
Zend/tests/gc_052.phpt
Normal 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
37
Zend/tests/gc_053.phpt
Normal 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)
|
||||
@@ -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) {
|
||||
|
||||
@@ -987,6 +987,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)) {
|
||||
|
||||
@@ -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 (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 */
|
||||
|
||||
21
Zend/zend_vm_execute.h
generated
21
Zend/zend_vm_execute.h
generated
@@ -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 (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 */
|
||||
|
||||
@@ -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,
|
||||
|
||||
4
Zend/zend_vm_opcodes.c
generated
4
Zend/zend_vm_opcodes.c
generated
@@ -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,
|
||||
|
||||
1
Zend/zend_vm_opcodes.h
generated
1
Zend/zend_vm_opcodes.h
generated
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user