[php-src] master: Fix GH-21691: OPcache CFG optimizer drops QM_ASSIGN feeding JMPZ/JMPZ_EX with IS_VAR

Ilia Alshanetsky via Ilija Tovilo <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Committer: Ilija Tovilo (iluuu1994)
Date: 2026-07-16T00:52:23+02:00

Commit: https://github.com/php/php-src/commit/6441f89857b3528e6b71a5f9ce6fa1ade3e67d7e
Raw diff: https://github.com/php/php-src/commit/6441f89857b3528e6b71a5f9ce6fa1ade3e67d7e.diff

Fix GH-21691: OPcache CFG optimizer drops QM_ASSIGN feeding JMPZ/JMPZ_EX with IS_VAR

The CFG optimizer (pass 5) substituted a QM_ASSIGN's source operand
into its consumer without checking the source's operand type. When
ASSIGN_REF produces IS_VAR and a QM_ASSIGN converts it to IS_TMP_VAR
before JMPZ/JMPNZ or JMPZ_EX/JMPNZ_EX, eliminating the QM_ASSIGN
leaves an IS_VAR operand on a consumer whose handler is declared
CONST|TMP|CV, producing "Invalid opcode 43/4/0" (or 46/4/0 for the
_EX variants).

Guard both substitution sites with src->op1_type != IS_VAR, folded
into the enclosing condition (per dstogov's review) so the BOOL_NOT
branch is defensively covered as well. Test exercises both JMPZ and
JMPZ_EX paths.

Closes GH-21691
Closes GH-21696

Changed paths:
  A  ext/opcache/tests/gh21691.phpt
  M  Zend/Optimizer/block_pass.c


Diff:

diff --git a/Zend/Optimizer/block_pass.c b/Zend/Optimizer/block_pass.c
index 02c28ead33e1..3775f8165c3e 100644
--- a/Zend/Optimizer/block_pass.c
+++ b/Zend/Optimizer/block_pass.c
@@ -648,7 +648,7 @@ static void zend_optimize_block(zend_basic_block *block, zend_op_array *op_array
 					} else if (opline->op1_type == IS_TMP_VAR &&
 					           !zend_bitset_in(used_ext, VAR_NUM(opline->op1.var))) {
 						src = VAR_SOURCE(opline->op1);
-						if (src) {
+						if (src && (src->op1_type != IS_VAR)) {
 							if (src->opcode == ZEND_BOOL_NOT) {
 								VAR_SOURCE(opline->op1) = NULL;
 								COPY_NODE(opline->op1, src->op1);
@@ -692,7 +692,7 @@ static void zend_optimize_block(zend_basic_block *block, zend_op_array *op_array
 					           (!zend_bitset_in(used_ext, VAR_NUM(opline->op1.var)) ||
 					            opline->result.var == opline->op1.var)) {
 						src = VAR_SOURCE(opline->op1);
-						if (src) {
+						if (src && (src->op1_type != IS_VAR)) {
 							if (src->opcode == ZEND_BOOL ||
 							    src->opcode == ZEND_QM_ASSIGN) {
 								VAR_SOURCE(opline->op1) = NULL;
diff --git a/ext/opcache/tests/gh21691.phpt b/ext/opcache/tests/gh21691.phpt
new file mode 100644
index 000000000000..3926b76c7a12
--- /dev/null
+++ b/ext/opcache/tests/gh21691.phpt
@@ -0,0 +1,20 @@
+--TEST--
+GH-21691 (OPcache CFG optimizer breaks reference returns with JMPZ/JMPZ_EX)
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+
+function &getData() {
+    return $x;
+}
+
+$data = &getData() && isset($data['key']);
+var_dump($data);
+
+?>
+--EXPECT--
+NULL
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.