[php-src] PR #23546: Fix zend_optimizer_replace_by_const() leaving stale OP2 uses behind

From: Date: Wed, 02 Sep 2026 15:28:30 +0000
Subject: [php-src] PR #23546: Fix zend_optimizer_replace_by_const() leaving stale OP2 uses behind
Groups: php.git-pulls 
Request: Send a blank email to git-pulls+get-38268@lists.php.net to get a copy of this message
Pull Request: https://github.com/php/php-src/pull/23546 Author: Mrmaxmeier Hi, we ran into a failing SSA integrity check with the fuzzer-function-jit fuzzing target: ```php <?php function test($v) { switch ($v) { case [$x] = (int)1.5: return "matched " . var_export($x, true); default: return "default"; } } var_dump(test(1)); var_dump(test(2)); ``` <details> <summary>Assertion backtrace for reproducer</summary> ``` /out/php-fuzz-function-jit: Running 1 inputs 100 time(s) each. Running: /testcase In function ::test (before dfa): var op2 of 3 (ZEND_IS_EQUAL) does not use/def an ssa var test: ; (lines=7, args=1, vars=2, tmps=4, ssa_vars=6, no_loops) ; (at SSA integrity verification) ; /tmp/fuzzer.php:2-7 ; return [null] RANGE[0..0] ; #0.CV0($v) NOVAL [undef] ; #1.CV1($x) NOVAL [undef] BB0: ; start lines=[0-4] ; to=(BB2, BB1) ; level=0 ; children=(BB1, BB2) 0000 #2.CV0($v) [any] = RECV 1 0001 #3.T4 [null] = FETCH_LIST_R int(1) int(0) 0002 ASSIGN #1.CV1($x) NOVAL [undef] -> #4.CV1($x) NOVAL [null] #3.T4 [null] 0003 #5.T2 [bool] = IS_EQUAL #2.CV0($v) [any] T3 0004 JMPNZ #5.T2 [bool] BB2 BB1: ; follow exit lines=[5-5] ; from=(BB0) ; idom=BB0 ; level=1 0005 RETURN null BB2: ; target exit lines=[6-6] ; from=(BB0) ; idom=BB0 ; level=1 0006 RETURN null php-fuzz-function-jit: /src/php-src/Zend/Optimizer/ssa_integrity.c:419: void ssa_verify_integrity(zend_op_array *, zend_ssa *, const char *): Assertion `0 && "SSA integrity verification failed"' failed. AddressSanitizer:DEADLYSIGNAL ================================================================= ==14==ERROR: AddressSanitizer: ABRT on unknown address 0x00000000000e (pc 0x7f53e68a200b bp 0x7f53e6a17588 sp 0x7fffe77bb9b0 T0) SCARINESS: 10 (signal) #0 0x7f53e68a200b in raise (/lib/x86_64-linux-gnu/libc.so.6+0x4300b) (BuildId: 5792732f783158c66fb4f3756458ca24e46e827d) #1 0x7f53e6881858 in abort (/lib/x86_64-linux-gnu/libc.so.6+0x22858) (BuildId: 5792732f783158c66fb4f3756458ca24e46e827d) #2 0x7f53e6881728 (/lib/x86_64-linux-gnu/libc.so.6+0x22728) (BuildId: 5792732f783158c66fb4f3756458ca24e46e827d) #3 0x7f53e6892fd5 in __assert_fail (/lib/x86_64-linux-gnu/libc.so.6+0x33fd5) (BuildId: 5792732f783158c66fb4f3756458ca24e46e827d) #4 0x561f569f7398 in ssa_verify_integrity /src/php-src/Zend/Optimizer/ssa_integrity.c:419:3 #5 0x561f569f90af in zend_dfa_optimize_op_array /src/php-src/Zend/Optimizer/dfa_pass.c:1075:3 #6 0x561f56ac2d78 in zend_optimize_script /src/php-src/Zend/Optimizer/zend_optimizer.c:1658:5 #7 0x561f56237b79 in cache_script_in_shared_memory /src/php-src/ext/opcache/ZendAccelerator.c:1598:2 #8 0x561f56239b6d in persistent_compile_file /src/php-src/ext/opcache/ZendAccelerator.c:2399:24 #9 0x561f5711538e in fuzzer_do_request_from_buffer /src/php-src/sapi/fuzzer/fuzzer-sapi.c:289:29 #10 0x561f57113954 in LLVMFuzzerTestOneInput /src/php-src/sapi/fuzzer/fuzzer-function-jit.c:32:2 [..] DEDUP_TOKEN: raise--abort-- SUMMARY: AddressSanitizer: ABRT (/lib/x86_64-linux-gnu/libc.so.6+0x4300b) (BuildId: 5792732f783158c66fb4f3756458ca24e46e827d) in raise ==14==ABORTING ``` </details> For opcodes that do not consume their OP1 operand (ZEND_FETCH_LIST_R and friends), the constant is propagated into *every* use of the temporary rather than just the first one, because the value stays alive past the first reader. That loop only ever looked at OP1 though, so a use of the same temporary as an OP2 operand was skipped and left referring to a temporary that no longer has a definition. In the reproducer 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. That use keeps the dangling reference, and SSA construction then fails its integrity check. The fix handles the OP2 case in the same loop. An OP2 operand is always consumed, so such a use is necessarily the last one and the loop can stop there. ZEND_FE_FETCH_R / ZEND_FE_FETCH_RW are the sole exception, where OP2 is a def rather than a use, so they are excluded. Thanks! --- _Found by the CISPA Fandango team while triaging findings in oss-fuzz harnesses._

« previous php.git-pulls (#38268) next »