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