Commit 53b80f07f0f for php.net
commit 53b80f07f0f4dc1ebdb2cc710bbf8b83e799616c
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Tue Jul 7 08:13:14 2026 -0400
Check the stack limit when calling internal functions (#22545)
zend_call_function invokes an internal callee's handler directly, with no
VM frame and without the stack-limit check the interpreter runs at its call
opcodes. An internal function that recurses through zend_call_function, such
as a self- or mutually-attached SPL iterator, never yields back to the VM,
so nothing bounds the recursion and the C stack overflows into a SEGV.
Check zend_call_stack_overflowed() before pushing the call frame and raise
the usual "Maximum call stack size reached" error on overflow. Running the
check ahead of the frame setup keeps the error path free of teardown.
Fixes GH-15672
Fixes GH-15911
diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c
index 71e0c56a51c..cb48e623437 100644
--- a/Zend/zend_execute_API.c
+++ b/Zend/zend_execute_API.c
@@ -861,6 +861,14 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
}
}
+#ifdef ZEND_CHECK_STACK_LIMIT
+ if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) {
+ zend_call_stack_size_error();
+ zend_release_fcall_info_cache(fci_cache);
+ return SUCCESS;
+ }
+#endif
+
call = zend_vm_stack_push_call_frame(call_info,
func, fci->param_count, object_or_called_scope);
uint32_t consumed_args = fci->param_count ? fci->consumed_args : 0;
diff --git a/ext/spl/tests/gh15672.phpt b/ext/spl/tests/gh15672.phpt
new file mode 100644
index 00000000000..ff3feda46f7
--- /dev/null
+++ b/ext/spl/tests/gh15672.phpt
@@ -0,0 +1,22 @@
+--TEST--
+GH-15672 (MultipleIterator attached to itself overflows the C stack)
+--SKIPIF--
+<?php
+if (!function_exists('zend_test_zend_call_stack_get')) die("skip zend_test_zend_call_stack_get() is not available");
+if (getenv("SKIP_SLOW_TESTS")) die('skip slow test');
+?>
+--EXTENSIONS--
+zend_test
+--INI--
+memory_limit=2G
+zend.max_allowed_stack_size=512K
+--FILE--
+<?php
+$m = new MultipleIterator();
+$m->attachIterator(new ArrayIterator([1, 2, 3]), "1");
+$m->attachIterator($m, 3);
+foreach ($m as $key => $value) {
+}
+?>
+--EXPECTREGEX--
+.*Maximum call stack size of \d+ bytes.*Infinite recursion\?.*thrown in .* on line \d+
diff --git a/ext/spl/tests/gh15911.phpt b/ext/spl/tests/gh15911.phpt
new file mode 100644
index 00000000000..502b2baf616
--- /dev/null
+++ b/ext/spl/tests/gh15911.phpt
@@ -0,0 +1,21 @@
+--TEST--
+GH-15911 (AppendIterator appended to itself overflows the C stack)
+--SKIPIF--
+<?php
+if (!function_exists('zend_test_zend_call_stack_get')) die("skip zend_test_zend_call_stack_get() is not available");
+if (getenv("SKIP_SLOW_TESTS")) die('skip slow test');
+?>
+--EXTENSIONS--
+zend_test
+--INI--
+memory_limit=2G
+zend.max_allowed_stack_size=512K
+--FILE--
+<?php
+$it = new AppendIterator();
+$it->append($it);
+foreach ($it as $v) {
+}
+?>
+--EXPECTREGEX--
+.*Maximum call stack size of \d+ bytes.*Infinite recursion\?.*thrown in .* on line \d+