diff --git a/Zend/Zend.m4 b/Zend/Zend.m4 index f651ee57d74..a1450e4f133 100644 --- a/Zend/Zend.m4 +++ b/Zend/Zend.m4 @@ -302,6 +302,28 @@ fi AC_MSG_CHECKING(whether to enable zend signal handling) AC_MSG_RESULT($ZEND_SIGNALS) +dnl Don't enable Zend Max Execution Timers by default until PHP 8.3 to not break the ABI +AC_ARG_ENABLE([zend-max-execution-timers], + [AS_HELP_STRING([--enable-zend-max-execution-timers], + [whether to enable zend max execution timers])], + [ZEND_MAX_EXECUTION_TIMERS=$enableval], + [ZEND_MAX_EXECUTION_TIMERS='no']) + +AS_CASE(["$host_alias"], [*linux*], [], [ZEND_MAX_EXECUTION_TIMERS='no']) + +PHP_CHECK_FUNC(timer_create, rt) +if test "$ac_cv_func_timer_create" != "yes"; then + ZEND_MAX_EXECUTION_TIMERS='no' +fi + +if test "$ZEND_MAX_EXECUTION_TIMERS" = "yes"; then + AC_DEFINE(ZEND_MAX_EXECUTION_TIMERS, 1, [Use zend max execution timers]) + CFLAGS="$CFLAGS -DZEND_MAX_EXECUTION_TIMERS" +fi + +AC_MSG_CHECKING(whether to enable zend max execution timers) +AC_MSG_RESULT($ZEND_MAX_EXECUTION_TIMERS) + ]) AC_ARG_ENABLE([gcc-global-regs], diff --git a/Zend/zend.c b/Zend/zend.c index 9bfd9178f52..78bde673d69 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -36,6 +36,7 @@ #include "zend_observer.h" #include "zend_fibers.h" #include "zend_call_stack.h" +#include "zend_max_execution_timer.h" #include "Optimizer/zend_optimizer.h" static size_t global_map_ptr_last = 0; @@ -802,6 +803,10 @@ static void executor_globals_ctor(zend_executor_globals *executor_globals) /* {{ executor_globals->stack_limit = (void*)0; executor_globals->stack_base = (void*)0; #endif +#ifdef ZEND_MAX_EXECUTION_TIMERS + executor_globals->pid = 0; + executor_globals->oldact = (struct sigaction){0}; +#endif } /* }}} */ @@ -826,6 +831,7 @@ static void zend_new_thread_end_handler(THREAD_T thread_id) /* {{{ */ #ifdef ZEND_CHECK_STACK_LIMIT zend_call_stack_init(); #endif + zend_max_execution_timer_init(); } /* }}} */ #endif @@ -1607,6 +1613,18 @@ ZEND_API ZEND_COLD ZEND_NORETURN void zend_error_noreturn(int type, const char * abort(); } +ZEND_API ZEND_COLD ZEND_NORETURN void zend_strerror_noreturn(int type, int errn, const char *message) +{ +#ifdef HAVE_STR_ERROR_R + char buf[1024]; + strerror_r(errn, buf, sizeof(buf)); +#else + char *buf = strerror(errn); +#endif + + zend_error_noreturn(type, "%s: %s (%d)", message, buf, errn); +} + ZEND_API ZEND_COLD void zend_error_zstr(int type, zend_string *message) { zend_string *filename; uint32_t lineno; diff --git a/Zend/zend.h b/Zend/zend.h index a0c0fc2126d..6e550843ea2 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -40,6 +40,7 @@ #include "zend_smart_string_public.h" #include "zend_signal.h" #include "zend_type_code.h" +#include "zend_max_execution_timer.h" #define zend_sprintf sprintf @@ -360,6 +361,9 @@ ZEND_API ZEND_COLD void zend_value_error(const char *format, ...) ZEND_ATTRIBUTE ZEND_COLD void zenderror(const char *error); +/* For internal C errors */ +ZEND_API ZEND_COLD ZEND_NORETURN void zend_strerror_noreturn(int type, int errn, const char *message); + /* The following #define is used for code duality in PHP for Engine 1 & 2 */ #define ZEND_STANDARD_CLASS_DEF_PTR zend_standard_class_def extern ZEND_API zend_class_entry *zend_standard_class_def; diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index c279cdc3caf..cf1be1b5ad1 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -44,6 +44,9 @@ #ifdef HAVE_UNISTD_H #include #endif +#ifdef ZEND_MAX_EXECUTION_TIMERS +#include +#endif ZEND_API void (*zend_execute_ex)(zend_execute_data *execute_data); ZEND_API void (*zend_execute_internal)(zend_execute_data *execute_data, zval *return_value); @@ -196,6 +199,7 @@ void init_executor(void) /* {{{ */ EG(filename_override) = NULL; EG(lineno_override) = -1; + zend_max_execution_timer_init(); zend_fiber_init(); zend_weakrefs_init(); @@ -413,6 +417,7 @@ void shutdown_executor(void) /* {{{ */ zend_shutdown_executor_values(fast_shutdown); zend_weakrefs_shutdown(); + zend_max_execution_timer_shutdown(); zend_fiber_shutdown(); zend_try { @@ -1367,8 +1372,35 @@ ZEND_API ZEND_NORETURN void ZEND_FASTCALL zend_timeout(void) /* {{{ */ /* }}} */ #ifndef ZEND_WIN32 +# ifdef ZEND_MAX_EXECUTION_TIMERS +static void zend_timeout_handler(int dummy, siginfo_t *si, void *uc) /* {{{ */ +{ +#ifdef ZTS + if (!tsrm_is_managed_thread()) { + fprintf(stderr, "zend_timeout_handler() called in a thread not managed by PHP. The expected signal handler will not be called. This is probably a bug.\n"); + + return; + } +#endif + + if (si->si_value.sival_ptr != &EG(max_execution_timer_timer)) { +#ifdef MAX_EXECUTION_TIMERS_DEBUG + fprintf(stderr, "Executing previous handler (if set) for unexpected signal SIGRTMIN received on thread %d\n", (pid_t) syscall(SYS_gettid)); +#endif + + if (EG(oldact).sa_sigaction) { + EG(oldact).sa_sigaction(dummy, si, uc); + + return; + } + if (EG(oldact).sa_handler) EG(oldact).sa_handler(dummy); + + return; + } +# else static void zend_timeout_handler(int dummy) /* {{{ */ { +# endif #ifdef ZTS if (!tsrm_is_managed_thread()) { fprintf(stderr, "zend_timeout_handler() called in a thread not managed by PHP. The expected signal handler will not be called. This is probably a bug.\n"); @@ -1474,6 +1506,21 @@ static void zend_set_timeout_ex(zend_long seconds, bool reset_signals) /* {{{ */ zend_error_noreturn(E_ERROR, "Could not queue new timer"); return; } +#elif defined(ZEND_MAX_EXECUTION_TIMERS) + zend_max_execution_timer_settime(seconds); + + if (reset_signals) { + sigset_t sigset; + struct sigaction act; + + act.sa_sigaction = zend_timeout_handler; + sigemptyset(&act.sa_mask); + act.sa_flags = SA_ONSTACK | SA_SIGINFO; + sigaction(SIGRTMIN, &act, NULL); + sigemptyset(&sigset); + sigaddset(&sigset, SIGRTMIN); + sigprocmask(SIG_UNBLOCK, &sigset, NULL); + } #elif defined(HAVE_SETITIMER) { struct itimerval t_r; /* timeout requested */ @@ -1539,6 +1586,8 @@ void zend_unset_timeout(void) /* {{{ */ } tq_timer = NULL; } +#elif ZEND_MAX_EXECUTION_TIMERS + zend_max_execution_timer_settime(0); #elif defined(HAVE_SETITIMER) if (EG(timeout_seconds)) { struct itimerval no_timeout; diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index 9243597c27c..71f1f8168ba 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -23,6 +23,7 @@ #include #include +#include #include "zend_globals_macros.h" @@ -39,6 +40,7 @@ #include "zend_multiply.h" #include "zend_arena.h" #include "zend_call_stack.h" +#include "zend_max_execution_timer.h" /* Define ZTS if you want a thread-safe Zend */ /*#undef ZTS*/ @@ -294,6 +296,12 @@ struct _zend_executor_globals { zend_ulong reserved_stack_size; #endif +#ifdef ZEND_MAX_EXECUTION_TIMERS + timer_t max_execution_timer_timer; + pid_t pid; + struct sigaction oldact; +#endif + void *reserved[ZEND_MAX_RESERVED_RESOURCES]; }; diff --git a/Zend/zend_max_execution_timer.c b/Zend/zend_max_execution_timer.c new file mode 100644 index 00000000000..b1c83e9cbb3 --- /dev/null +++ b/Zend/zend_max_execution_timer.c @@ -0,0 +1,103 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Kévin Dunglas | + +----------------------------------------------------------------------+ + */ + +#ifdef ZEND_MAX_EXECUTION_TIMERS + +#include +#include +#include +#include +#include +#include +#include + +#include "zend.h" +#include "zend_globals.h" + +// Musl Libc defines this macro, glibc does not +// According to "man 2 timer_create" this field should always be available, but it's not: https://sourceware.org/bugzilla/show_bug.cgi?id=27417 +# ifndef sigev_notify_thread_id +# define sigev_notify_thread_id _sigev_un._tid +# endif + +ZEND_API void zend_max_execution_timer_init(void) /* {{{ */ +{ + struct sigevent sev; + sev.sigev_notify = SIGEV_THREAD_ID; + sev.sigev_value.sival_ptr = &EG(max_execution_timer_timer); + sev.sigev_signo = SIGRTMIN; + sev.sigev_notify_thread_id = (pid_t) syscall(SYS_gettid); + + EG(pid) = getpid(); + // Measure wall time instead of CPU time as originally planned now that it is possible https://github.com/php/php-src/pull/6504#issuecomment-1370303727 + if (timer_create(CLOCK_BOOTTIME, &sev, &EG(max_execution_timer_timer)) != 0) { + zend_strerror_noreturn(E_ERROR, errno, "Could not create timer"); + } + +# ifdef MAX_EXECUTION_TIMERS_DEBUG + fprintf(stderr, "Timer %#jx created on thread %d\n", (uintmax_t) EG(max_execution_timer_timer), sev.sigev_notify_thread_id); +# endif + + sigaction(sev.sigev_signo, NULL, &EG(oldact)); +} +/* }}} */ + +void zend_max_execution_timer_settime(zend_long seconds) /* {{{ }*/ +{ + /* Timer not initialized or shutdown. */ + if (!EG(pid)) { + return; + } + + timer_t timer = EG(max_execution_timer_timer); + + struct itimerspec its; + its.it_value.tv_sec = seconds; + its.it_value.tv_nsec = its.it_interval.tv_sec = its.it_interval.tv_nsec = 0; + +# ifdef MAX_EXECUTION_TIMERS_DEBUG + fprintf(stderr, "Setting timer %#jx on thread %d (%ld seconds)...\n", (uintmax_t) timer, (pid_t) syscall(SYS_gettid), seconds); +# endif + + if (timer_settime(timer, 0, &its, NULL) != 0) { + zend_strerror_noreturn(E_ERROR, errno, "Could not set timer"); + } +} +/* }}} */ + +void zend_max_execution_timer_shutdown(void) /* {{{ */ +{ + /* Don't try to delete a timer created before a call to fork() */ + if (EG(pid) != getpid()) { + return; + } + + EG(pid) = 0; + + timer_t timer = EG(max_execution_timer_timer); + +# ifdef MAX_EXECUTION_TIMERS_DEBUG + fprintf(stderr, "Deleting timer %#jx on thread %d...\n", (uintmax_t) timer, (pid_t) syscall(SYS_gettid)); +# endif + + int err = timer_delete(timer); + if (err != 0) { + zend_strerror_noreturn(E_ERROR, errno, "Could not delete timer"); + } +} +/* }}}} */ + +#endif diff --git a/Zend/zend_max_execution_timer.h b/Zend/zend_max_execution_timer.h new file mode 100644 index 00000000000..789f50e6b7d --- /dev/null +++ b/Zend/zend_max_execution_timer.h @@ -0,0 +1,36 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Kévin Dunglas | + +----------------------------------------------------------------------+ + */ + +#ifndef ZEND_MAX_EXECUTION_TIMER_H +#define ZEND_MAX_EXECUTION_TIMER_H + +# ifdef ZEND_MAX_EXECUTION_TIMERS + +#include "zend_long.h" + +/* Must be called after calls to fork() */ +ZEND_API void zend_max_execution_timer_init(void); +void zend_max_execution_timer_settime(zend_long seconds); +void zend_max_execution_timer_shutdown(void); + +# else + +#define zend_max_execution_timer_init() +#define zend_max_execution_timer_settime(seconds) +#define zend_max_execution_timer_shutdown() + +# endif +#endif diff --git a/configure.ac b/configure.ac index 3ac52ef98a5..ce981d1b871 100644 --- a/configure.ac +++ b/configure.ac @@ -636,6 +636,7 @@ asprintf \ nanosleep \ memmem \ memrchr \ +strerror_r \ ) AX_FUNC_WHICH_GETHOSTBYNAME_R @@ -1722,7 +1723,7 @@ PHP_ADD_SOURCES(Zend, \ zend_virtual_cwd.c zend_ast.c zend_objects.c zend_object_handlers.c zend_objects_API.c \ zend_default_classes.c zend_inheritance.c zend_smart_str.c zend_cpuinfo.c zend_gdb.c \ zend_observer.c zend_system_id.c zend_enum.c zend_fibers.c zend_atomic.c \ - zend_rc_debug.c \ + zend_rc_debug.c zend_max_execution_timer.c \ Optimizer/zend_optimizer.c \ Optimizer/pass1.c \ Optimizer/pass3.c \ diff --git a/ext/pcntl/pcntl.c b/ext/pcntl/pcntl.c index 226ebce196f..bd4c32ca408 100644 --- a/ext/pcntl/pcntl.c +++ b/ext/pcntl/pcntl.c @@ -58,6 +58,8 @@ #include "pcntl_arginfo.h" +#include "Zend/zend_max_execution_timer.h" + ZEND_DECLARE_MODULE_GLOBALS(pcntl) static PHP_GINIT_FUNCTION(pcntl); @@ -184,6 +186,8 @@ PHP_FUNCTION(pcntl_fork) if (id == -1) { PCNTL_G(last_error) = errno; php_error_docref(NULL, E_WARNING, "Error %d", errno); + } else if (id == 0) { + zend_max_execution_timer_init(); } RETURN_LONG((zend_long) id); diff --git a/ext/standard/info.c b/ext/standard/info.c index 0c4081d2a91..9d914508e97 100644 --- a/ext/standard/info.c +++ b/ext/standard/info.c @@ -898,6 +898,12 @@ PHPAPI ZEND_COLD void php_print_info(int flag) efree(descr); } +#ifdef ZEND_MAX_EXECUTION_TIMERS + php_info_print_table_row(2, "Zend Max Execution Timers", "enabled" ); +#else + php_info_print_table_row(2, "Zend Max Execution Timers", "disabled" ); +#endif + #ifdef HAVE_IPV6 php_info_print_table_row(2, "IPv6 Support", "enabled" ); #else diff --git a/ext/standard/tests/general_functions/phpinfo.phpt b/ext/standard/tests/general_functions/phpinfo.phpt index 5f4d99ffbaf..57ba13bb7bf 100644 --- a/ext/standard/tests/general_functions/phpinfo.phpt +++ b/ext/standard/tests/general_functions/phpinfo.phpt @@ -34,6 +34,7 @@ Thread Safety => %s%A Zend Signal Handling => %s Zend Memory Manager => %s Zend Multibyte Support => %s +Zend Max Execution Timers => %s IPv6 Support => %s DTrace Support => %s