[php-src] PR #25046: Fix GH-24792: Assignment-result writes compile but fail with invalid opcode
| From: | ChrisAbner | Date: | Fri, 30 Oct 2026 03:14:14 +0000 |
| Subject: | [php-src] PR #25046: Fix GH-24792: Assignment-result writes compile but fail with invalid opcode | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-40077@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/25046
Author: ChrisAbner
Fixes GH-24792.
Since the VAR|TMP overhaul (6173a9a109),
zend_compile_var_inner() compiles
ZEND_AST_ASSIGN and ZEND_AST_ASSIGN_REF for every fetch type. When an
assignment is the container of a write, such as ($a = [1])[0] = 2, unset(($a =
[1])[0]) or $r = &($a = [1])[0], the compiler emits a write fetch on the TMP
result of the assignment. That combination has no handler, so the script compiles and then dies with
Invalid opcode. PHP 8.5 rejected all of these at compile time with "Cannot use
temporary expression in write context". This patch restores that error.
The overhaul added these cases so that a destructuring assignment with references can be passed by
reference (f([&$x] = $arr)). It originally had an assertion limiting them to that
case, which a01a8e72ac removed. The patch keeps compiling assignments when by_ref is
set or when the fetch is not a write (W/RW/UNSET), and
otherwise falls through to the existing default branch and its error. This puts the
check in the one place every write container goes through. Checking in each caller (dim, prop,
unset, inc/dec, reference fetch) would have needed one check per caller. As a side effect, ($a
=& $b)[0] = 2 and ([&$x] = $arr)[0] = 5 are compile errors again. PHP
8.5 rejected them too; 8.6.0RC4 started executing them. If you would rather keep those working in
8.6, I can narrow the check.
**Verification.** Debug NTS build of PHP-8.6 (e2fa0c9f7f, --disable-all
--enable-cli), compared with the official php:8.5-cli (8.5.12) and
php:8.6.0RC4-cli images:
| Code | 8.5.12 | 8.6.0RC4 | PHP-8.6 + patch |
|---|---|---|---|
| ($a = [1])[0] = 2; | compile error | Invalid opcode 23/2/1 | compile
error |
| ($a = new stdClass)->p = 1; | compile error | Invalid opcode 24/2/1 |
compile error |
| ($a = [1])[0] += 2; | compile error | Invalid opcode 27/2/1 | compile
error |
| ($a = [1])[0]++; | compile error | Invalid opcode 87/2/1 | compile error
|
| unset(($a = [1])[0]); | compile error | Invalid opcode 75/2/1 | compile
error |
| $r = &($a = [1])[0]; | compile error | Invalid opcode 84/2/1 |
compile error |
| f(($a = [1])[0]); with f(&$x) | compile error | Invalid opcode
84/2/1 | compile error |
| ($a =& $b)[0] = 2; | compile error | runs | compile error |
| ([&$x] = $arr)[0] = 5; | compile error | runs | compile error |
| f([&$x] = $arr); / f($a =& $b); with f(&$x) |
works | works | works |
| isset(($a = [1])[0]), ($a = [1])[5] ?? 'd', ($v =
['k' => 'v'])['k'] | works | works | works |
Out of 30 variants in total, all 30 match 8.5.12 with the patch, with and without OPcache + tracing
JIT. Without the patch, 15 fail with Invalid opcode in 8.6.0RC4 and on master.
| Tests | Result |
|---|---|
| New Zend/tests/gh24792*.phpt (9) on 8.5.12 / 8.6.0RC4 / patched | 9 pass / 8 fail / 9
pass |
| Zend/tests + tests, patched | 6301 passed, 0 failed |
| Zend/tests + tests + ext/opcache/tests, patched,
opcache.enable_cli=1, opcache.jit=tracing,
opcache.protect_memory=1 | 7275 passed, 0 failed |
Not covered: foreach (($a =& $b) as &$v) { $v = 3; } writes through to
$b in 8.5 but not in 8.6.0RC4, with or without this patch. It is a separate behaviour
change from the same overhaul and is left alone here. I only tested on Linux x86_64 NTS debug
builds, not ZTS or ASAN builds of the patch. The bug itself also reproduces on the official ZTS RC4
image.