From a23ac9ed3603c06ba72a5ce5ed534dedabb693b6 Mon Sep 17 00:00:00 2001 From: Mrmaxmeier Date: Mon, 24 Aug 2026 16:26:09 +0200 Subject: [PATCH] Fix zend_optimizer_replace_by_const() leaving stale OP2 uses behind For opcodes that do not consume their OP1 operand, the constant is propagated into every use of the temporary rather than just the first one. That loop only ever looked at OP1, so a use of the same temporary as an OP2 operand was left referring to a temporary that no longer has a definition: function test($v) { switch ($v) { case [$x] = (int)1.5: break; } } The ZEND_CAST is folded and rewritten into the ZEND_FETCH_LIST_R of the list assignment, but the comparison emitted for the case expression uses the same temporary as its OP2 and keeps the dangling reference, which makes SSA construction fail its integrity check. An OP2 operand is always consumed (ZEND_FE_FETCH_R/RW, where OP2 is a def rather than a use, is the sole exception), so such a use is the last one and the loop can stop there. Assisted-By: Claude Opus 5 --- NEWS | 2 ++ Zend/Optimizer/zend_optimizer.c | 11 +++++++++ ...zer_function_jit_ssa_case_list_assign.phpt | 24 +++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 ext/opcache/tests/fuzzer_function_jit_ssa_case_list_assign.phpt diff --git a/NEWS b/NEWS index 608bacc29109..303a5c32f99f 100644 --- a/NEWS +++ b/NEWS @@ -54,6 +54,8 @@ PHP NEWS . Fixed a tracing JIT crash when compiling a side trace for a method of a class that could not be stored in the inheritance cache. (GH-21710) (Arnaud, iliaal) + . Fixed zend_optimizer_replace_by_const() leaving stale OP2 uses behind. + (Mrmaxmeier) - PDO: . Fixed a leak when a persistent connection failed a liveness check diff --git a/Zend/Optimizer/zend_optimizer.c b/Zend/Optimizer/zend_optimizer.c index 9017572118e2..78665f3b4efc 100644 --- a/Zend/Optimizer/zend_optimizer.c +++ b/Zend/Optimizer/zend_optimizer.c @@ -666,6 +666,17 @@ bool zend_optimizer_replace_by_const(zend_op_array *op_array, if (is_last) { break; } + } else if (opline->op2_type == type && opline->op2.var == var + && opline->opcode != ZEND_FE_FETCH_R + && opline->opcode != ZEND_FE_FETCH_RW) { + /* An OP2 operand is always consumed, so this is the last use. + * OP2 of FE_FETCH is a def rather than a use, stop there. */ + Z_TRY_ADDREF_P(val); + if (!zend_optimizer_update_op2_const(op_array, opline, val)) { + zval_ptr_dtor(val); + return false; + } + break; } opline++; } diff --git a/ext/opcache/tests/fuzzer_function_jit_ssa_case_list_assign.phpt b/ext/opcache/tests/fuzzer_function_jit_ssa_case_list_assign.phpt new file mode 100644 index 000000000000..c3f24d2a73fc --- /dev/null +++ b/ext/opcache/tests/fuzzer_function_jit_ssa_case_list_assign.phpt @@ -0,0 +1,24 @@ +--TEST-- +zend_optimizer_replace_by_const(): OP2 uses of a temporary kept alive by FETCH_LIST_R +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=disable +--FILE-- + +--EXPECT-- +string(12) "matched NULL" +string(7) "default"