From 6b0e7c5d6c190939f10ca46a52ef3bdb7a7f8f63 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Wed, 4 Nov 2026 11:08:47 +0100 Subject: [PATCH 1/3] Throw UnwindExit on timeout --- ...execution_time_timeout_no_destructors.phpt | 27 +++++++++++++ ...max_execution_time_timeout_no_finally.phpt | 38 +++++++++++++++++++ .../max_execution_time_timeout_shutdown.phpt | 18 +++++++++ Zend/zend_execute.h | 2 +- Zend/zend_execute_API.c | 19 +++++++++- Zend/zend_vm_def.h | 1 + Zend/zend_vm_execute.h | 2 + ext/opcache/jit/zend_jit_ir.c | 20 +++++++--- 8 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 Zend/tests/max_execution_time_timeout_no_destructors.phpt create mode 100644 Zend/tests/max_execution_time_timeout_no_finally.phpt create mode 100644 Zend/tests/max_execution_time_timeout_shutdown.phpt diff --git a/Zend/tests/max_execution_time_timeout_no_destructors.phpt b/Zend/tests/max_execution_time_timeout_no_destructors.phpt new file mode 100644 index 000000000000..22824b390edd --- /dev/null +++ b/Zend/tests/max_execution_time_timeout_no_destructors.phpt @@ -0,0 +1,27 @@ +--TEST-- +max_execution_time timeout does not call destructors +--SKIPIF-- + +--INI-- +max_execution_time=1 +--FILE-- + +--EXPECTF-- +Fatal error: Maximum execution time of 1 second exceeded %s +shutdown function called diff --git a/Zend/tests/max_execution_time_timeout_no_finally.phpt b/Zend/tests/max_execution_time_timeout_no_finally.phpt new file mode 100644 index 000000000000..4c5747a04c34 --- /dev/null +++ b/Zend/tests/max_execution_time_timeout_no_finally.phpt @@ -0,0 +1,38 @@ +--TEST-- +max_execution_time timeout skips finally blocks +--SKIPIF-- + +--INI-- +max_execution_time=1 +--FILE-- +current(); + +$gen2 = (function () { + try { + while (true) {} + } finally { + echo "finally executed (2)\n"; + } + yield; +})(); + +try { + foreach ($gen2 as $value) {} +} finally { + echo "finally executed (3)\n"; +} + +?> +--EXPECTF-- +Fatal error: Maximum execution time of 1 second exceeded %s diff --git a/Zend/tests/max_execution_time_timeout_shutdown.phpt b/Zend/tests/max_execution_time_timeout_shutdown.phpt new file mode 100644 index 000000000000..e482f75e6a62 --- /dev/null +++ b/Zend/tests/max_execution_time_timeout_shutdown.phpt @@ -0,0 +1,18 @@ +--TEST-- +max_execution_time timeout during a shutdown function +--SKIPIF-- + +--INI-- +max_execution_time=1 +--FILE-- + +--EXPECTF-- +Fatal error: Maximum execution time of 1 second exceeded %s diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index 3e750e8cf30a..2b538281ffed 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -526,7 +526,7 @@ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_cannot_pass_by_reference_ex(const zen ZEND_API void zend_set_timeout(zend_long seconds, bool reset_signals); ZEND_API void zend_unset_timeout(void); -ZEND_NORETURN ZEND_API void ZEND_FASTCALL zend_timeout(void); +ZEND_API ZEND_COLD void ZEND_FASTCALL zend_timeout(void); ZEND_API zend_class_entry *zend_fetch_class(zend_string *class_name, uint32_t fetch_type); ZEND_API zend_class_entry *zend_fetch_class_with_scope(zend_string *class_name, uint32_t fetch_type, zend_class_entry *scope); ZEND_API zend_class_entry *zend_fetch_class_by_name(zend_string *class_name, zend_string *lcname, uint32_t fetch_type); diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index d8cf2d016ee8..6576d60e1b15 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -1494,7 +1494,8 @@ ZEND_API zend_result zend_eval_string_ex(const char *str, zval *retval_ptr, cons static void zend_set_timeout_ex(zend_long seconds, bool reset_signals); -ZEND_NORETURN ZEND_API void ZEND_FASTCALL zend_timeout(void) /* {{{ */ +/* zend_timeout() is called by the VM interrupt handler when EG(timed_out) is set */ +ZEND_API ZEND_COLD void ZEND_FASTCALL zend_timeout(void) /* {{{ */ { #if defined(PHP_WIN32) # ifndef ZTS @@ -1514,7 +1515,21 @@ ZEND_NORETURN ZEND_API void ZEND_FASTCALL zend_timeout(void) /* {{{ */ zend_set_timeout_ex(0, true); #endif - zend_error_noreturn(E_ERROR, "Maximum execution time of " ZEND_LONG_FMT " second%s exceeded", EG(timeout_seconds), EG(timeout_seconds) == 1 ? "" : "s"); + /* Trigger error but do not bail */ + zend_error(E_ERROR | E_DONT_BAIL, "Maximum execution time of " ZEND_LONG_FMT " second%s exceeded", EG(timeout_seconds), EG(timeout_seconds) == 1 ? "" : "s"); + + /* Do not call destructors */ + zend_objects_store_mark_destructed(&EG(objects_store)); + + /* An exception may already be in flight, and zend_throw_unwind_exit() + * does not allow that */ + if (EG(exception)) { + OBJ_RELEASE(EG(exception)); + EG(exception) = NULL; + } + + ZEND_ASSERT(EG(current_execute_data)); + zend_throw_unwind_exit(); } /* }}} */ diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 6d6053022f58..43b770060847 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -10743,6 +10743,7 @@ ZEND_VM_HELPER(zend_interrupt_helper, ANY, ANY) #endif if (atomic_load(&EG(timed_out))) { zend_timeout(); + HANDLE_EXCEPTION(); } else if (ZEND_HAS_OOM()) { zend_handle_delayed_oom(); HANDLE_EXCEPTION(); diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index f6deb7be7e69..8af9a8d18a8a 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -4055,6 +4055,7 @@ static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV #endif if (atomic_load(&EG(timed_out))) { zend_timeout(); + HANDLE_EXCEPTION(); } else if (ZEND_HAS_OOM()) { zend_handle_delayed_oom(); HANDLE_EXCEPTION(); @@ -56934,6 +56935,7 @@ static zend_never_inline ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend #endif if (atomic_load(&EG(timed_out))) { zend_timeout(); + HANDLE_EXCEPTION(); } else if (ZEND_HAS_OOM()) { zend_handle_delayed_oom(); HANDLE_EXCEPTION(); diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index b8f838134328..8bf93a1ac27a 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -2052,23 +2052,30 @@ static int zend_jit_exception_handler_free_op2_stub(zend_jit_ctx *jit) static int zend_jit_interrupt_handler_stub(zend_jit_ctx *jit) { ir_ref if_timeout, if_exception; + ir_ref end_inputs = IR_UNUSED; // EX(opline) = opline ir_STORE(jit_EX(opline), jit_IP(jit)); ir_STORE(jit_EG(vm_interrupt), ir_CONST_U8(0)); - if_timeout = ir_IF(ir_EQ(ir_LOAD_U8(jit_EG(timed_out)), ir_CONST_U8(0))); + + // if (EG(timed_out)) { + if_timeout = ir_IF(ir_LOAD_U8(jit_EG(timed_out))); + ir_IF_TRUE(if_timeout); + ir_CALL(IR_VOID, ir_CONST_FUNC(zend_timeout)); + ir_CALL(IR_VOID, ir_CONST_FUNC(zend_jit_exception_in_interrupt_handler_helper)); + // LOAD_OPLINE() + jit_STORE_IP(jit, ir_LOAD_A(jit_EX(opline))); + ir_END_list(end_inputs); ir_IF_FALSE(if_timeout); - ir_CALL(IR_VOID, ir_CONST_FUNC(zend_timeout)); // noreturn - ir_MERGE_WITH_EMPTY_TRUE(if_timeout); - // if (ZEND_HAS_OOM()) { + // } else if (ZEND_HAS_OOM()) { ir_ref if_delayed_oom = ir_IF(ir_LOAD_U8(ir_ADD_OFFSET(jit_EG(delayed_oom), offsetof(zend_oom_info, state)))); ir_IF_TRUE(if_delayed_oom); ir_CALL(IR_VOID, ir_CONST_FC_FUNC(zend_handle_delayed_oom)); // LOAD_OPLINE() jit_STORE_IP(jit, ir_LOAD_A(jit_EX(opline))); - ir_ref if_delayed_oom_end = ir_END(); + ir_END_list(end_inputs); ir_IF_FALSE(if_delayed_oom); if (zend_interrupt_function) { @@ -2082,7 +2089,8 @@ static int zend_jit_interrupt_handler_stub(zend_jit_ctx *jit) jit_STORE_IP(jit, ir_LOAD_A(jit_EX(opline))); } - ir_MERGE_WITH(if_delayed_oom_end); + ir_END_list(end_inputs); + ir_MERGE_list(end_inputs); if (GCC_GLOBAL_REGS || ZEND_VM_KIND == ZEND_VM_KIND_TAILCALL) { zend_jit_tailcall_handler(jit, ir_LOAD_A(jit_IP(jit))); From 80aff096ec09401357750cef681656098fdc0485 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Wed, 4 Nov 2026 18:26:14 +0100 Subject: [PATCH 2/3] zend_timeout() may be called without an EG(current_execute_data) during shutdown --- Zend/zend_execute_API.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 6576d60e1b15..3a9b5f707503 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -1494,7 +1494,7 @@ ZEND_API zend_result zend_eval_string_ex(const char *str, zval *retval_ptr, cons static void zend_set_timeout_ex(zend_long seconds, bool reset_signals); -/* zend_timeout() is called by the VM interrupt handler when EG(timed_out) is set */ +/* zend_timeout() is called by the VM interrupt handler or by zend_call_function() when EG(timed_out) is set */ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_timeout(void) /* {{{ */ { #if defined(PHP_WIN32) @@ -1521,15 +1521,17 @@ ZEND_API ZEND_COLD void ZEND_FASTCALL zend_timeout(void) /* {{{ */ /* Do not call destructors */ zend_objects_store_mark_destructed(&EG(objects_store)); - /* An exception may already be in flight, and zend_throw_unwind_exit() - * does not allow that */ if (EG(exception)) { OBJ_RELEASE(EG(exception)); - EG(exception) = NULL; } + EG(exception) = zend_create_unwind_exit(); - ZEND_ASSERT(EG(current_execute_data)); - zend_throw_unwind_exit(); + if (EG(current_execute_data)) { + /* zend_call_function() may call zend_timeout() without EG(current_execute_data) + * when executing shutdown functions */ + EG(opline_before_exception) = EG(current_execute_data)->opline; + EG(current_execute_data)->opline = EG(exception_op); + } } /* }}} */ From a960274be258ba018245024ef0ef70df17786699 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Wed, 4 Nov 2026 18:36:16 +0100 Subject: [PATCH 3/3] Move new tests along with pre-existing ones --- .../basic/timeout_variation_11.phpt | 6 +++++- .../basic/timeout_variation_12.phpt | 2 +- .../basic/timeout_variation_13.phpt | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) rename Zend/tests/max_execution_time_timeout_shutdown.phpt => tests/basic/timeout_variation_11.phpt (64%) rename Zend/tests/max_execution_time_timeout_no_destructors.phpt => tests/basic/timeout_variation_12.phpt (88%) rename Zend/tests/max_execution_time_timeout_no_finally.phpt => tests/basic/timeout_variation_13.phpt (92%) diff --git a/Zend/tests/max_execution_time_timeout_shutdown.phpt b/tests/basic/timeout_variation_11.phpt similarity index 64% rename from Zend/tests/max_execution_time_timeout_shutdown.phpt rename to tests/basic/timeout_variation_11.phpt index e482f75e6a62..29adf06bb459 100644 --- a/Zend/tests/max_execution_time_timeout_shutdown.phpt +++ b/tests/basic/timeout_variation_11.phpt @@ -1,5 +1,5 @@ --TEST-- -max_execution_time timeout during a shutdown function +Timeout within shutdown function, user function variation --SKIPIF-- --EXPECTF-- diff --git a/Zend/tests/max_execution_time_timeout_no_destructors.phpt b/tests/basic/timeout_variation_12.phpt similarity index 88% rename from Zend/tests/max_execution_time_timeout_no_destructors.phpt rename to tests/basic/timeout_variation_12.phpt index 22824b390edd..af58b3d6049a 100644 --- a/Zend/tests/max_execution_time_timeout_no_destructors.phpt +++ b/tests/basic/timeout_variation_12.phpt @@ -1,5 +1,5 @@ --TEST-- -max_execution_time timeout does not call destructors +Timeout does not call destructors --SKIPIF--