Commit 342a7465b3c for php
commit 342a7465b3c5fb888a34492cf4d3dd402e13f4f0
Author: Mrmaxmeier <Mrmaxmeier@gmail.com>
Date: Mon Aug 24 16:26:08 2026 +0200
Fix call_stack buffer overflow in zend_analyze_calls()
The call stack was sized as op_array->last / 2, on the assumption that every
call needs at least an INIT and a DO_FCALL opcode. That assumption does not
hold after the optimizer has removed the DO_FCALL opcodes as dead code, in
which case nothing pops the stack again:
```php
function test() {
new A(new B(new C(new D(match ([]) { 1 => 2 }))));
}
```
The match arm never matches, so everything behind the ZEND_MATCH_ERROR is
removed and the optimized op_array is just four ZEND_NEWs followed by the
ZEND_MATCH_ERROR. The buffer then holds two entries while four are pushed.
Size the stack by op_array->last instead, which is the only safe upper bound
once the pushes and pops are no longer guaranteed to be balanced.
Assisted-By: Claude <noreply@anthropic.com>
Closes GH-23454.
diff --git a/NEWS b/NEWS
index da8362ce1ed..1de00a09ded 100644
--- a/NEWS
+++ b/NEWS
@@ -61,6 +61,7 @@ PHP NEWS
loop-invariant addition). (Ilia Alshanetsky)
. Fixed OSS-Fuzz #5674034779193344 (Read of uninitialized memory in
is_cacheable_stream_path()). (ndossche)
+ . Fix zend_analyze_calls() call_stack buffer overrun. (Mrmaxmeier)
- PDO:
. Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid
diff --git a/Zend/Optimizer/zend_call_graph.c b/Zend/Optimizer/zend_call_graph.c
index 8a2f8ea2a7e..cbb4c906a97 100644
--- a/Zend/Optimizer/zend_call_graph.c
+++ b/Zend/Optimizer/zend_call_graph.c
@@ -54,7 +54,10 @@ ZEND_API void zend_analyze_calls(zend_arena **arena, zend_script *script, uint32
ALLOCA_FLAG(use_heap);
bool is_prototype;
- call_stack = do_alloca((op_array->last / 2) * sizeof(zend_call_info*), use_heap);
+ // Note: Reserve one call stack slot per operation. Each opcode pushes at
+ // most one entry to the call stack, and (with dead code elimination) it's
+ // possible to never pop from the stack.
+ call_stack = do_alloca(op_array->last * sizeof(zend_call_info*), use_heap);
call_info = NULL;
while (opline != end) {
switch (opline->opcode) {
diff --git a/ext/opcache/tests/opt/call_graph_stack_overflow.phpt b/ext/opcache/tests/opt/call_graph_stack_overflow.phpt
new file mode 100644
index 00000000000..2044daf06c8
--- /dev/null
+++ b/ext/opcache/tests/opt/call_graph_stack_overflow.phpt
@@ -0,0 +1,17 @@
+--TEST--
+zend_analyze_calls(): call_stack overflow when dead code elimination removed the DO_FCALLs
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=0x7FFEBFFF
+--FILE--
+<?php
+function test() {
+ new A(new B(new C(new D(match ([]) { 1 => 2 }))));
+}
+echo "OK\n";
+?>
+--EXPECT--
+OK