From 4991c60a9c9584772f22a1809e9a89c0750b00f2 Mon Sep 17 00:00:00 2001 From: Mrmaxmeier Date: Mon, 24 Aug 2026 16:26:08 +0200 Subject: [PATCH] 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: 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 Opus 5 --- NEWS | 2 ++ Zend/Optimizer/zend_call_graph.c | 5 ++++- ...unction_jit_call_graph_stack_overflow.phpt | 20 +++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 ext/opcache/tests/fuzzer_function_jit_call_graph_stack_overflow.phpt diff --git a/NEWS b/NEWS index a2c65685b4ce..a1d4c51a7eb3 100644 --- a/NEWS +++ b/NEWS @@ -36,6 +36,8 @@ PHP NEWS . Fixed a tracing JIT crash when compiling a side trace for a method of a class that could not be stored in the inheritance cache. (GH-21710) (Arnaud, iliaal) + . Fixed a call_stack buffer overflow in zend_analyze_calls() when dead code + elimination has removed the DO_FCALL opcodes. (Mrmaxmeier) - PDO: . Fixed a leak when a persistent connection failed a liveness check diff --git a/Zend/Optimizer/zend_call_graph.c b/Zend/Optimizer/zend_call_graph.c index 8a2f8ea2a7e1..cbb4c906a97e 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/fuzzer_function_jit_call_graph_stack_overflow.phpt b/ext/opcache/tests/fuzzer_function_jit_call_graph_stack_overflow.phpt new file mode 100644 index 000000000000..ef4fd6a1b923 --- /dev/null +++ b/ext/opcache/tests/fuzzer_function_jit_call_graph_stack_overflow.phpt @@ -0,0 +1,20 @@ +--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.jit=disable +--ENV-- +USE_ZEND_ALLOC=0 +USE_TRACKED_ALLOC=1 +--FILE-- + 2 })))); +} +echo "OK\n"; +?> +--EXPECT-- +OK