[php-src] PR #23538: Fix block pass dropping a JMP across an unreachable_free block

From: Date: Wed, 02 Sep 2026 09:45:00 +0000
Subject: [php-src] PR #23538: Fix block pass dropping a JMP across an unreachable_free block
Groups: php.git-pulls 
Request: Send a blank email to git-pulls+get-38260@lists.php.net to get a copy of this message
Pull Request: https://github.com/php/php-src/pull/23538 Author: Mrmaxmeier Hi, we ran into an assertion failure with the fuzzer-function-jit fuzzing target: ```php <?php 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]; } test(true, ["y"], ["z"]); ``` (On release builds this might turn into a double-free or break program semantics) <details> <summary>Assertion and backtrace for reproducer</summary> ``` /out/php-fuzz-function-jit: Running 1 inputs 100 time(s) each. Running: /testcase php-fuzz-function-jit: /src/php-src/Zend/zend_opcode.c:754: void emit_live_range_raw(zend_op_array *, uint32_t, uint32_t, uint32_t, uint32_t): Assertion `start < end' failed. AddressSanitizer:DEADLYSIGNAL ================================================================= ==14==ERROR: AddressSanitizer: ABRT on unknown address 0x00000000000e (pc 0x7fdce9bc200b bp 0x7fdce9d37588 sp 0x7fff78b89ab0 T0) SCARINESS: 10 (signal) #0 0x7fdce9bc200b in raise (/lib/x86_64-linux-gnu/libc.so.6+0x4300b) (BuildId: 5792732f783158c66fb4f3756458ca24e46e827d) #1 0x7fdce9ba1858 in abort (/lib/x86_64-linux-gnu/libc.so.6+0x22858) (BuildId: 5792732f783158c66fb4f3756458ca24e46e827d) #2 0x7fdce9ba1728 (/lib/x86_64-linux-gnu/libc.so.6+0x22728) (BuildId: 5792732f783158c66fb4f3756458ca24e46e827d) #3 0x7fdce9bb2fd5 in __assert_fail (/lib/x86_64-linux-gnu/libc.so.6+0x33fd5) (BuildId: 5792732f783158c66fb4f3756458ca24e46e827d) #4 0x55d2e3aacdf7 in emit_live_range_raw /src/php-src/Zend/zend_opcode.c:754:2 #5 0x55d2e3aa91db in zend_calc_live_ranges /src/php-src/Zend/zend_opcode.c:1027:6 #6 0x55d2e34c4c31 in zend_optimize_script /src/php-src/Zend/Optimizer/zend_optimizer.c #7 0x55d2e2c37b79 in cache_script_in_shared_memory /src/php-src/ext/opcache/ZendAccelerator.c:1598:2 #8 0x55d2e2c39b6d in persistent_compile_file /src/php-src/ext/opcache/ZendAccelerator.c:2399:24 #9 0x55d2e3b1538e in fuzzer_do_request_from_buffer /src/php-src/sapi/fuzzer/fuzzer-sapi.c:289:29 #10 0x55d2e3b13954 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> The block pass marks an unreachable block that frees a loop variable created in a *reachable* block as ZEND_BB_UNREACHABLE_FREE, and still emits it: the FREE opcodes it contains determine where the live range of that variable ends. Two places in the block pass drop a ZEND_JMP when it targets the block that physically follows it. Both walk forward over the block list and skip every block that is not ZEND_BB_REACHABLE. That skips the ZEND_BB_UNREACHABLE_FREE blocks as well, even though those do get emitted: ```c while (next < end && !(next->flags & ZEND_BB_REACHABLE)) { next++; } ``` So the pass removes the jump while an unreachable_free block still physically sits between the two, and control falls straight into a FREE opcode that is never meant to run. In the reproducer, the continue 2 path frees the switch subject twice and leaves an inverted live range behind. The fix teaches both walks that a non-empty ZEND_BB_UNREACHABLE_FREE block still separates a block from its successor, so the jump stays. Thanks! --- _Found by the CISPA Fandango team while triaging findings in oss-fuzz harnesses._

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