[php-src] PR #23538: Fix block pass dropping a JMP across an unreachable_free block
| From: | Mrmaxmeier | 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._