From 97cca8f8f826d986db8f9a6b03410dd8e338f99b Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Thu, 29 Oct 2026 13:11:05 -0600 Subject: [PATCH] Fix potential infinite recursion in HYBRID/TAILCALL VMs 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. --- NEWS | 2 ++ Zend/tests/vm_interrupt_rearm.phpt | 26 ++++++++++++++++++++++++++ Zend/zend_vm_def.h | 6 +++++- Zend/zend_vm_execute.h | 6 +++++- ext/zend_test/php_test.h | 1 + ext/zend_test/test.c | 29 +++++++++++++++++++++++++++++ 6 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 Zend/tests/vm_interrupt_rearm.phpt diff --git a/NEWS b/NEWS index a654cec2f398..57d6e4694d07 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 000000000000..605cb59086d7 --- /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-- + +--EXPECT-- +int(1000) diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index df255c5a868f..53f33e0ce5a0 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 4b313f20cd39..258567bb34ed 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -3901,7 +3901,11 @@ static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL zend_interrupt_he } } - 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/ext/zend_test/php_test.h b/ext/zend_test/php_test.h index 9ab27f5a54f6..ee2e334fb55c 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 d9d80bf6ccf1..a11fc54af465 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; }