Commit 5f4d73a697b for php

commit 5f4d73a697bf83ea9d25f542c3641e8b0c0951f8
Author: Levi Morrison <levi.morrison@datadoghq.com>
Date:   Fri Oct 30 08:46:22 2026 -0600

    Fix potential infinite recursion in HYBRID/TAILCALL VMs (#25039)

    The recursion will result in a stack overflow on HYBRID VMs and
    infinite recursion in TAILCALL VMs.

    The property that the VM makes guaranteed forward progress is useful
    for testing but it's also helpful to guard against for edge cases in
    production as well.

diff --git a/NEWS b/NEWS
index a654cec2f39..57d6e4694d0 100644
--- a/NEWS
+++ b/NEWS
@@ -189,6 +189,8 @@ PHP                                                                        NEWS
     after restart. (Pavel Gushcha)
   . Fix opcache.interned_strings_buffer being 0 in threads started later (ZTS).
     (Pavel Gushcha)
+  . Fixed potential infinite recursion/hang (HYBRID/TAILCALL respectively)
+    when zend_interrupt_function resets EG(vm_interrupt).  (Levi Morrison)

 - OpenSSL:
   . Fixed bug GH-24784 (openssl_pkcs7_read() and openssl_cms_read() overwrite
diff --git a/Zend/tests/vm_interrupt_rearm.phpt b/Zend/tests/vm_interrupt_rearm.phpt
new file mode 100644
index 00000000000..605cb59086d
--- /dev/null
+++ b/Zend/tests/vm_interrupt_rearm.phpt
@@ -0,0 +1,26 @@
+--TEST--
+VM interrupt re-armed by the interrupt handler must not stop execution from making progress
+--DESCRIPTION--
+zend_test.rearm_vm_interrupt installs a zend_interrupt_function that sets
+EG(vm_interrupt) again every time it runs, so an interrupt is always pending.
+After handling an interrupt the VM has to run the next opline before it checks
+the flag again, otherwise it calls the handler forever (or recurses until the
+stack overflows).
+--EXTENSIONS--
+zend_test
+--INI--
+zend_test.rearm_vm_interrupt=1
+--FILE--
+<?php
+function inc(int $x): int {
+    return $x + 1;
+}
+
+$n = 0;
+for ($i = 0; $i < 1000; $i++) {
+    $n = inc($n);
+}
+var_dump($n);
+?>
+--EXPECT--
+int(1000)
diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index df255c5a868..53f33e0ce5a 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -10446,7 +10446,11 @@ ZEND_VM_HELPER(zend_interrupt_helper, ANY, ANY)

 			}
 		}
-		ZEND_VM_ENTER();
+		/* Like ZEND_VM_ENTER() but don't check for interrupts yet to avoid recursion if the
+		 * interrupt handler re-sets the vm_interrupt flag. */
+		execute_data = EG(current_execute_data);
+		LOAD_OPLINE();
+		ZEND_VM_LEAVE();
 	}
 	ZEND_VM_CONTINUE();
 }
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index 4b313f20cd3..258567bb34e 100644
Binary files a/Zend/zend_vm_execute.h and b/Zend/zend_vm_execute.h differ
diff --git a/ext/zend_test/php_test.h b/ext/zend_test/php_test.h
index 9ab27f5a54f..ee2e334fb55 100644
--- a/ext/zend_test/php_test.h
+++ b/ext/zend_test/php_test.h
@@ -56,6 +56,7 @@ ZEND_BEGIN_MODULE_GLOBALS(zend_test)
 	HashTable *global_weakmap;
 	int replace_zend_execute_ex;
 	int register_passes;
+	bool rearm_vm_interrupt;
 	bool print_stderr_mshutdown;
 	zend_long limit_copy_file_range;
 	int observe_opline_in_zendmm;
diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c
index d9d80bf6ccf..a11fc54af46 100644
--- a/ext/zend_test/test.c
+++ b/ext/zend_test/test.c
@@ -1355,10 +1355,24 @@ static ZEND_METHOD(_ZendTestMagicCallForward, __call)
 	zval_ptr_dtor(&rv);
 }

+static void (*zend_test_prev_interrupt_function)(zend_execute_data *execute_data);
+
+/* The VM clears EG(vm_interrupt) before calling this, so setting it again
+ * leaves an interrupt pending at all times. The VM must still make progress. */
+static void zend_test_rearm_interrupt_function(zend_execute_data *execute_data)
+{
+	zend_atomic_bool_store_ex(&EG(vm_interrupt), true);
+
+	if (zend_test_prev_interrupt_function) {
+		zend_test_prev_interrupt_function(execute_data);
+	}
+}
+
 PHP_INI_BEGIN()
 	PHP_INI_ENTRY("zend_test.register_system_entropy", "0", PHP_INI_SYSTEM, NULL)
 	STD_PHP_INI_BOOLEAN("zend_test.replace_zend_execute_ex", "0", PHP_INI_SYSTEM, OnUpdateBool, replace_zend_execute_ex, zend_zend_test_globals, zend_test_globals)
 	STD_PHP_INI_BOOLEAN("zend_test.register_passes", "0", PHP_INI_SYSTEM, OnUpdateBool, register_passes, zend_zend_test_globals, zend_test_globals)
+	STD_PHP_INI_BOOLEAN("zend_test.rearm_vm_interrupt", "0", PHP_INI_SYSTEM, OnUpdateBool, rearm_vm_interrupt, zend_zend_test_globals, zend_test_globals)
 	STD_PHP_INI_BOOLEAN("zend_test.print_stderr_mshutdown", "0", PHP_INI_SYSTEM, OnUpdateBool, print_stderr_mshutdown, zend_zend_test_globals, zend_test_globals)
 #ifdef HAVE_COPY_FILE_RANGE
 	STD_PHP_INI_ENTRY("zend_test.limit_copy_file_range", "-1", PHP_INI_ALL, OnUpdateLong, limit_copy_file_range, zend_zend_test_globals, zend_test_globals)
@@ -1573,6 +1587,11 @@ PHP_MINIT_FUNCTION(zend_test)
 		zend_optimizer_register_pass(pass2);
 	}

+	if (ZT_G(rearm_vm_interrupt)) {
+		zend_test_prev_interrupt_function = zend_interrupt_function;
+		zend_interrupt_function = zend_test_rearm_interrupt_function;
+	}
+
 	zend_test_observer_init(INIT_FUNC_ARGS_PASSTHRU);
 	zend_test_mm_custom_handlers_minit(INIT_FUNC_ARGS_PASSTHRU);
 	zend_test_fiber_init();
@@ -1592,6 +1611,11 @@ PHP_MSHUTDOWN_FUNCTION(zend_test)

 	zend_test_observer_shutdown(SHUTDOWN_FUNC_ARGS_PASSTHRU);

+	if (zend_interrupt_function == zend_test_rearm_interrupt_function) {
+		zend_interrupt_function = zend_test_prev_interrupt_function;
+		zend_test_prev_interrupt_function = NULL;
+	}
+
 	if (ZT_G(print_stderr_mshutdown)) {
 		fprintf(stderr, "[zend_test] MSHUTDOWN\n");
 	}
@@ -1605,6 +1629,11 @@ PHP_RINIT_FUNCTION(zend_test)
 	zend_hash_init(ZT_G(global_weakmap), 8, NULL, ZVAL_PTR_DTOR, 0);
 	ZT_G(observer_nesting_depth) = 0;
 	zend_test_mm_custom_handlers_rinit();
+
+	if (ZT_G(rearm_vm_interrupt)) {
+		zend_atomic_bool_store_ex(&EG(vm_interrupt), true);
+	}
+
 	return SUCCESS;
 }