Skip to content

Commit fe7c60b

Browse files
committed
Fix block pass dropping a JMP across an unreachable_free block
An unreachable block that frees a loop variable created in a reachable block is marked ZEND_BB_UNREACHABLE_FREE and is still emitted, because the FREE opcodes it contains determine where the live range of that variable ends. Two places that decide whether a ZEND_JMP can be dropped because it targets the block that physically follows skipped over every block that is not ZEND_BB_REACHABLE, including those. The jump was therefore removed even though an unreachable_free block still sat between the two, and control fell straight into a FREE opcode that was never meant to be executed: function test($a, $b, $c) { do { if ($a) { switch ($b[0]) { case 'x': switch ($c[0]) { default: return "returned"; } default: continue 2; } } } while (false); return $b[0]; } Taking the "continue 2" path freed the switch subject twice, and the resulting live range was inverted, which also tripped the start < end assertion in emit_live_range_raw(). Assisted-By: Claude <noreply@anthropic.com>
1 parent 8723e66 commit fe7c60b

2 files changed

Lines changed: 50 additions & 1 deletion

File tree

‎Zend/Optimizer/block_pass.c‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -950,7 +950,12 @@ static void assemble_code_blocks(zend_cfg *cfg, zend_op_array *op_array, zend_op
950950
if (opline->opcode == ZEND_JMP) {
951951
zend_basic_block *next = b + 1;
952952

953-
while (next < end && !(next->flags & ZEND_BB_REACHABLE)) {
953+
/* Unreachable blocks that are kept alive for their loop var
954+
* frees are emitted as well, so they still separate this block
955+
* from its successor. */
956+
while (next < end
957+
&& !(next->flags & ZEND_BB_REACHABLE)
958+
&& !((next->flags & ZEND_BB_UNREACHABLE_FREE) && next->len != 0)) {
954959
next++;
955960
}
956961
if (next < end && next == blocks + b->successors[0]) {
@@ -1149,6 +1154,11 @@ static zend_always_inline zend_basic_block *get_next_block(const zend_cfg *cfg,
11491154
return NULL;
11501155
} else if (next_block->flags & ZEND_BB_REACHABLE) {
11511156
break;
1157+
} else if ((next_block->flags & ZEND_BB_UNREACHABLE_FREE) && next_block->len != 0) {
1158+
/* This block is unreachable, but it is still emitted to keep the
1159+
* live range of a loop var alive, so it separates the block from
1160+
* whatever follows it. */
1161+
return NULL;
11521162
}
11531163
next_block++;
11541164
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
--TEST--
2+
Block pass must not strip a JMP across an unreachable block that is kept for its loop var frees
3+
--EXTENSIONS--
4+
opcache
5+
--INI--
6+
opcache.enable=1
7+
opcache.enable_cli=1
8+
--FILE--
9+
<?php
10+
function test($a, $b, $c) {
11+
do {
12+
if ($a) {
13+
switch ($b[0]) {
14+
case 'x':
15+
switch ($c[0]) {
16+
default:
17+
return "returned";
18+
}
19+
default:
20+
continue 2;
21+
}
22+
}
23+
} while (false);
24+
return $b[0];
25+
}
26+
27+
// Take the "continue 2" path with a refcounted switch subject.
28+
var_dump(test(true, [uniqid("p") !== "" ? "y" . uniqid("") : ""], ["z"]) !== "");
29+
// Take the "case 'x'" path.
30+
var_dump(test(true, ["x"], ["z"]));
31+
// Take the path that skips the switch entirely.
32+
var_dump(test(false, ["y"], ["z"]));
33+
echo "OK\n";
34+
?>
35+
--EXPECT--
36+
bool(true)
37+
string(8) "returned"
38+
string(1) "y"
39+
OK

0 commit comments

Comments
 (0)