[php-src] PR #23454: Fix zend_analyze_calls() call_stack buffer overrun
| From: | Mrmaxmeier | Date: | Tue, 25 Aug 2026 14:09:41 +0000 |
| Subject: | [php-src] PR #23454: Fix zend_analyze_calls() call_stack buffer overrun | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-38132@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/23454
Author: Mrmaxmeier
Hi,
we ran into an out-of-bounds write with the
php-fuzz-function-jit fuzzing target:
```php
<?php
function test() {
new A(new B(new C(new D(match ([]) { 1 => 2 }))));
}
```
<details>
<summary>ASAN backtrace for reproducer</summary>
```
/out/php-fuzz-function-jit: Running 1 inputs 100 time(s) each.
Running: /testcase
=================================================================
==14==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7b62989e2e60 at pc 0x556927643788
bp 0x7ffca0c77cd0 sp 0x7ffca0c77cc8
WRITE of size 8 at 0x7b62989e2e60 thread T0
SCARINESS: 42 (8-byte-write-heap-buffer-overflow)
#0 0x556927643787 in zend_analyze_calls /src/php-src/Zend/Optimizer/zend_call_graph.c:100:22
#1 0x5569276440fb in zend_analyze_call_graph /src/php-src/Zend/Optimizer/zend_call_graph.c:253:3
#2 0x5569276c28c2 in zend_optimize_script /src/php-src/Zend/Optimizer/zend_optimizer.c:1631:6
#3 0x556926e37b79 in cache_script_in_shared_memory
/src/php-src/ext/opcache/ZendAccelerator.c:1598:2
#4 0x556926e39b6d in persistent_compile_file /src/php-src/ext/opcache/ZendAccelerator.c:2399:24
#5 0x556927d1538e in fuzzer_do_request_from_buffer /src/php-src/sapi/fuzzer/fuzzer-sapi.c:289:29
#6 0x556927d13954 in LLVMFuzzerTestOneInput /src/php-src/sapi/fuzzer/fuzzer-function-jit.c:32:2
[..]
DEDUP_TOKEN: zend_analyze_calls--zend_analyze_call_graph--zend_optimize_script
0x7b62989e2e60 is located 0 bytes after 16-byte region [0x7b62989e2e50,0x7b62989e2e60)
allocated by thread T0 here:
#0 0x5569269a9df4 in malloc /src/llvm-project/compiler-rt/lib/asan/asan_malloc_linux.cpp:67:3
#1 0x5569276e0231 in tracked_malloc /src/php-src/Zend/zend_alloc.c:3016:14
#2 0x556927642767 in zend_analyze_calls /src/php-src/Zend/Optimizer/zend_call_graph.c:53:15
#3 0x5569276440fb in zend_analyze_call_graph /src/php-src/Zend/Optimizer/zend_call_graph.c:253:3
#4 0x5569276c28c2 in zend_optimize_script /src/php-src/Zend/Optimizer/zend_optimizer.c:1631:6
#5 0x556926e37b79 in cache_script_in_shared_memory
/src/php-src/ext/opcache/ZendAccelerator.c:1598:2
#6 0x556926e39b6d in persistent_compile_file /src/php-src/ext/opcache/ZendAccelerator.c:2399:24
#7 0x556927d1538e in fuzzer_do_request_from_buffer /src/php-src/sapi/fuzzer/fuzzer-sapi.c:289:29
#8 0x556927d13954 in LLVMFuzzerTestOneInput /src/php-src/sapi/fuzzer/fuzzer-function-jit.c:32:2
[..]
DEDUP_TOKEN: __interceptor_malloc--tracked_malloc--zend_analyze_calls
SUMMARY: AddressSanitizer: heap-buffer-overflow /src/php-src/Zend/Optimizer/zend_call_graph.c:100:22
in zend_analyze_calls
```
(Note: The buffer is a do_alloca(), so this can either be a stack or heap buffer
overflow depending on allocator behaviour. In this setup, ASAN reports it as a heap overflow.)
</details>
zend_analyze_calls() assumes that the call_stack is at most
op_array->last / 2 deep:
https://github.com/php/php-src/blob/5dcff379188117c965db4d2888a3ef8b20193e8e/Zend/Optimizer/zend_call_graph.c#L53
The implicit assumption here is that every call needs at least two opcodes, an INIT_*
that pushes an entry and a DO_FCALL that pops it again. That assumption stops holding
when the optimizer removes DO_FCALL opcodes as dead code.
This PR allocates op_array->last entries for call_stack instead,
assuming that each opcode pushes at most one call stack entry.
Thanks!
---
_Found by the CISPA Fandango team while triaging findings in oss-fuzz harnesses._