From 5014ac90852bd4676ab21ac85977ca646d3ef940 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 30 Jun 2026 19:59:14 +0200 Subject: [PATCH 1/4] EINTR handling --- Zend/zend.c | 2 + Zend/zend.h | 8 + Zend/zend_execute.h | 2 + Zend/zend_execute_API.c | 9 + Zend/zend_hrtime.h | 7 +- ext/pcntl/pcntl.c | 67 +++++- ext/pcntl/pcntl.stub.php | 6 + ext/pcntl/pcntl_arginfo.h | 13 +- ext/pcntl/pcntl_decl.h | 13 +- ext/pcntl/php_pcntl.h | 2 + .../signal_return_interrupt_pending.phpt | 38 ++++ .../signal_return_interrupt_syscall.phpt | 50 +++++ .../tests/signal_return_restart_syscall.phpt | 50 +++++ main/network.c | 64 ++---- main/php_deadline.h | 92 ++++++++ main/php_network.h | 32 ++- main/streams/xp_socket.c | 199 ++++++++++++------ 17 files changed, 520 insertions(+), 134 deletions(-) create mode 100644 ext/pcntl/tests/signal_return_interrupt_pending.phpt create mode 100644 ext/pcntl/tests/signal_return_interrupt_syscall.phpt create mode 100644 ext/pcntl/tests/signal_return_restart_syscall.phpt create mode 100644 main/php_deadline.h diff --git a/Zend/zend.c b/Zend/zend.c index 07692db85196..e2093b1ad4a2 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -91,6 +91,7 @@ ZEND_API FILE *(*zend_fopen)(zend_string *filename, zend_string **opened_path); ZEND_API zend_result (*zend_stream_open_function)(zend_file_handle *handle); ZEND_API void (*zend_ticks_function)(int ticks); ZEND_API void (*zend_interrupt_function)(zend_execute_data *execute_data); +ZEND_API zend_signal_interrupt_result (*zend_signal_interrupt_function)(void); ZEND_API void (*zend_error_cb)(int type, zend_string *error_filename, const uint32_t error_lineno, zend_string *message); void (*zend_printf_to_smart_string)(smart_string *buf, const char *format, va_list ap); void (*zend_printf_to_smart_str)(smart_str *buf, const char *format, va_list ap); @@ -972,6 +973,7 @@ void zend_startup(zend_utility_functions *utility_functions) /* {{{ */ zend_resolve_path = utility_functions->resolve_path_function; zend_interrupt_function = NULL; + zend_signal_interrupt_function = zend_signal_interrupt; #ifdef HAVE_DTRACE /* build with dtrace support */ diff --git a/Zend/zend.h b/Zend/zend.h index 0d5303192b57..f90493ed01e6 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -364,6 +364,14 @@ extern ZEND_API void (*zend_ticks_function)(int ticks); */ extern ZEND_API void (*zend_interrupt_function)(zend_execute_data *execute_data); +typedef enum { + ZEND_SIGNAL_RESTART, /* Restart the interrupted syscall */ + ZEND_SIGNAL_INTERRUPT, /* Do not restart */ +} zend_signal_interrupt_result; + +/* Called when a syscall is interrupted by a signal. Not null. */ +extern ZEND_API zend_signal_interrupt_result (*zend_signal_interrupt_function)(void); + extern ZEND_API void (*zend_error_cb)(int type, zend_string *error_filename, const uint32_t error_lineno, zend_string *message); extern ZEND_API void (*zend_on_timeout)(int seconds); extern ZEND_API zend_result (*zend_stream_open_function)(zend_file_handle *handle); diff --git a/Zend/zend_execute.h b/Zend/zend_execute.h index eb73b3f3de9e..d3692f9cad7d 100644 --- a/Zend/zend_execute.h +++ b/Zend/zend_execute.h @@ -475,6 +475,8 @@ ZEND_API zend_class_entry *zend_get_executed_scope(void); ZEND_API bool zend_is_executing(void); ZEND_API zend_never_inline ZEND_COLD void ZEND_FASTCALL zend_cannot_pass_by_reference(uint32_t arg_num); +ZEND_API zend_signal_interrupt_result zend_signal_interrupt(void); + ZEND_API void zend_set_timeout(zend_long seconds, bool reset_signals); ZEND_API void zend_unset_timeout(void); ZEND_API ZEND_NORETURN void ZEND_FASTCALL zend_timeout(void); diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 88b0a485750a..65132453494d 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -1431,6 +1431,15 @@ ZEND_API zend_result zend_eval_string_ex(const char *str, zval *retval_ptr, cons } /* }}} */ +ZEND_API zend_signal_interrupt_result zend_signal_interrupt(void) +{ + if (zend_atomic_bool_load_ex(&EG(timed_out))) { + return ZEND_SIGNAL_INTERRUPT; + } + + return ZEND_SIGNAL_RESTART; +} + static void zend_set_timeout_ex(zend_long seconds, bool reset_signals); ZEND_API ZEND_NORETURN void ZEND_FASTCALL zend_timeout(void) /* {{{ */ diff --git a/Zend/zend_hrtime.h b/Zend/zend_hrtime.h index 6d802caacfe0..9ee4ea43d210 100644 --- a/Zend/zend_hrtime.h +++ b/Zend/zend_hrtime.h @@ -54,6 +54,8 @@ #elif defined(_AIX) # undef ZEND_HRTIME_PLATFORM_AIX # define ZEND_HRTIME_PLATFORM_AIX 1 +#else +# error #endif #define ZEND_HRTIME_AVAILABLE (ZEND_HRTIME_PLATFORM_POSIX || ZEND_HRTIME_PLATFORM_WINDOWS || ZEND_HRTIME_PLATFORM_APPLE_MACH_ABSOLUTE || ZEND_HRTIME_PLATFORM_APPLE_GETTIME_NSEC || ZEND_HRTIME_PLATFORM_HPUX || ZEND_HRTIME_PLATFORM_AIX) @@ -76,6 +78,7 @@ ZEND_API extern clockid_t zend_hrtime_posix_clock_id; #endif +#define ZEND_HRTIME_T_MAX UINT64_MAX #define ZEND_NANO_IN_SEC UINT64_C(1000000000) typedef uint64_t zend_hrtime_t; @@ -93,7 +96,7 @@ static zend_always_inline zend_hrtime_t zend_hrtime(void) #elif ZEND_HRTIME_PLATFORM_APPLE_MACH_ABSOLUTE return (zend_hrtime_t)mach_absolute_time() * zend_hrtime_timerlib_info.numer / zend_hrtime_timerlib_info.denom; #elif ZEND_HRTIME_PLATFORM_POSIX - struct timespec ts = { .tv_sec = 0, .tv_nsec = 0 }; + struct timespec ts; clock_gettime(zend_hrtime_posix_clock_id, &ts); return ((zend_hrtime_t) ts.tv_sec * (zend_hrtime_t)ZEND_NANO_IN_SEC) + ts.tv_nsec; #elif ZEND_HRTIME_PLATFORM_HPUX @@ -104,7 +107,7 @@ static zend_always_inline zend_hrtime_t zend_hrtime(void) time_base_to_time(&t, TIMEBASE_SZ); return (zend_hrtime_t) t.tb_high * (zend_hrtime_t)NANO_IN_SEC + t.tb_low; #else - return 0; +# error #endif } diff --git a/ext/pcntl/pcntl.c b/ext/pcntl/pcntl.c index 794e75a1716e..63a732adb202 100644 --- a/ext/pcntl/pcntl.c +++ b/ext/pcntl/pcntl.c @@ -148,11 +148,14 @@ typedef psetid_t cpu_set_t; #define LONG_CONST(c) (zend_long) c +#include "Zend/zend_bitset.h" #include "Zend/zend_enum.h" #include "Zend/zend_max_execution_timer.h" #include "pcntl_arginfo.h" #include "pcntl_decl.h" + +static zend_class_entry *SignalReturn_ce; static zend_class_entry *QosClass_ce; ZEND_DECLARE_MODULE_GLOBALS(pcntl) @@ -183,12 +186,14 @@ ZEND_GET_MODULE(pcntl) #endif static void (*orig_interrupt_function)(zend_execute_data *execute_data); +static zend_signal_interrupt_result (*orig_signal_interrupt_function)(void); static void pcntl_signal_handler(int, siginfo_t*, void*); static void pcntl_siginfo_to_zval(int, siginfo_t*, zval*); -static void pcntl_signal_dispatch(void); +static zend_signal_interrupt_result pcntl_signal_dispatch(void); static void pcntl_signal_dispatch_tick_function(int dummy_int, void *dummy_pointer); static void pcntl_interrupt_function(zend_execute_data *execute_data); +static zend_signal_interrupt_result pcntl_signal_interrupt_function(void); static PHP_GINIT_FUNCTION(pcntl) { @@ -213,15 +218,19 @@ PHP_RINIT_FUNCTION(pcntl) PCNTL_G(num_signals) = SIGRTMAX + 1; } #endif + PCNTL_G(restart_syscalls) = ecalloc(zend_bitset_len(PCNTL_G(num_signals)), ZEND_BITSET_ELM_SIZE); return SUCCESS; } PHP_MINIT_FUNCTION(pcntl) { + SignalReturn_ce = register_class_Pcntl_SignalReturn(); QosClass_ce = register_class_Pcntl_QosClass(); register_pcntl_symbols(module_number); orig_interrupt_function = zend_interrupt_function; zend_interrupt_function = pcntl_interrupt_function; + orig_signal_interrupt_function = zend_signal_interrupt_function; + zend_signal_interrupt_function = pcntl_signal_interrupt_function; return SUCCESS; } @@ -252,6 +261,8 @@ PHP_RSHUTDOWN_FUNCTION(pcntl) efree(sig); } + efree(PCNTL_G(restart_syscalls)); + return SUCCESS; } @@ -850,13 +861,21 @@ PHP_FUNCTION(pcntl_signal) RETURN_THROWS(); } - /* Register with the OS first so that on failure we don't record a handler that was never installed */ - if (php_signal4(signo, pcntl_signal_handler, (int) restart_syscalls, 1) == (void *)SIG_ERR) { + /* Register with the OS first so that on failure we don't record a handler that was never installed. + * Always clear SA_RESTART so that interrupted syscalls return EINTR; zend_signal_interrupt_function + * decides whether to restart based on the handler return value and restart_syscalls. */ + if (php_signal4(signo, pcntl_signal_handler, false, 1) == (void *)SIG_ERR) { PCNTL_G(last_error) = errno; php_error_docref(NULL, E_WARNING, "Error assigning signal"); RETURN_FALSE; } + if (restart_syscalls) { + zend_bitset_incl(PCNTL_G(restart_syscalls), signo); + } else { + zend_bitset_excl(PCNTL_G(restart_syscalls), signo); + } + /* Add the function name to our signal table */ handle = zend_hash_index_update(&PCNTL_G(php_signal_table), signo, handle); Z_TRY_ADDREF_P(handle); @@ -1351,15 +1370,16 @@ static void pcntl_signal_handler(int signo, siginfo_t *siginfo, void *context) } } -void pcntl_signal_dispatch(void) +zend_signal_interrupt_result pcntl_signal_dispatch(void) { zval params[2], *handle, retval; struct php_pcntl_pending_signal *queue, *next; sigset_t mask; sigset_t old_mask; + bool interrupt = false; if(!PCNTL_G(pending_signals)) { - return; + return ZEND_SIGNAL_RESTART; } /* Mask all signals */ @@ -1367,9 +1387,10 @@ void pcntl_signal_dispatch(void) sigprocmask(SIG_BLOCK, &mask, &old_mask); /* Bail if the queue is empty or if we are already playing the queue */ + // TODO: For the purpose of EINTR handling, processing next signals here would be useful if (!PCNTL_G(head) || PCNTL_G(processing_signal_queue)) { sigprocmask(SIG_SETMASK, &old_mask, NULL); - return; + return ZEND_SIGNAL_RESTART; } /* Prevent switching fibers when handling signals */ @@ -1382,7 +1403,6 @@ void pcntl_signal_dispatch(void) PCNTL_G(head) = NULL; /* simple stores are atomic */ PCNTL_G(tail) = NULL; - /* Allocate */ while (queue) { if ((handle = zend_hash_index_find(&PCNTL_G(php_signal_table), queue->signo)) != NULL) { if (Z_TYPE_P(handle) != IS_LONG) { @@ -1390,12 +1410,27 @@ void pcntl_signal_dispatch(void) array_init(¶ms[1]); pcntl_siginfo_to_zval(queue->signo, &queue->siginfo, ¶ms[1]); - /* Call php signal handler - Note that we do not report errors, and we ignore the return value */ call_user_function(NULL, NULL, handle, &retval, 2, params); - zval_ptr_dtor(&retval); zval_ptr_dtor(¶ms[1]); + if (Z_TYPE(retval) == IS_OBJECT && Z_OBJCE(retval) == SignalReturn_ce) { + if (zend_enum_fetch_case_id(Z_OBJ(retval)) == ZEND_ENUM_Pcntl_SignalReturn_Interrupt) { + interrupt = true; + } else if (!zend_bitset_in(PCNTL_G(restart_syscalls), queue->signo)) { + interrupt = true; + } + } else if (Z_TYPE(retval) > IS_NULL) { + zend_type_error("Signal handler must return a Pcntl\\SignalReturn or no value, %s returned", + zend_zval_value_name(&retval)); + interrupt = true; + } else if (!zend_bitset_in(PCNTL_G(restart_syscalls), queue->signo)) { + interrupt = true; + } + + zval_ptr_dtor(&retval); + if (EG(exception)) { + interrupt = true; break; } } @@ -1425,11 +1460,13 @@ void pcntl_signal_dispatch(void) /* return signal mask to previous state */ sigprocmask(SIG_SETMASK, &old_mask, NULL); + + return interrupt ? ZEND_SIGNAL_INTERRUPT : ZEND_SIGNAL_RESTART; } static void pcntl_signal_dispatch_tick_function(int dummy_int, void *dummy_pointer) { - return pcntl_signal_dispatch(); + pcntl_signal_dispatch(); } /* {{{ Enable/disable asynchronous signal handling and return the old setting. */ @@ -1908,3 +1945,13 @@ static void pcntl_interrupt_function(zend_execute_data *execute_data) orig_interrupt_function(execute_data); } } + +static zend_signal_interrupt_result pcntl_signal_interrupt_function(void) +{ + zend_signal_interrupt_result result = pcntl_signal_dispatch(); + if (orig_signal_interrupt_function() == ZEND_SIGNAL_INTERRUPT) { + result = ZEND_SIGNAL_INTERRUPT; + } + + return result; +} diff --git a/ext/pcntl/pcntl.stub.php b/ext/pcntl/pcntl.stub.php index 4a4b8fe86931..8e71457c6865 100644 --- a/ext/pcntl/pcntl.stub.php +++ b/ext/pcntl/pcntl.stub.php @@ -1112,6 +1112,12 @@ function pcntl_setqos_class(Pcntl\QosClass $qos_class = Pcntl\QosClass::Default) namespace Pcntl { + enum SignalReturn + { + case Default; + case Interrupt; + } + enum QosClass { case UserInteractive; diff --git a/ext/pcntl/pcntl_arginfo.h b/ext/pcntl/pcntl_arginfo.h index 2da7c8ad5db8..0d12b7a1f052 100644 --- a/ext/pcntl/pcntl_arginfo.h +++ b/ext/pcntl/pcntl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit pcntl.stub.php instead. - * Stub hash: 04e7b30c6fb23cf6ce6bc26fe094fd5b4dbfe826 + * Stub hash: 2be6f0046170cad02be1d01227b749412af7b51f * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_pcntl_fork, 0, 0, IS_LONG, 0) @@ -662,6 +662,17 @@ static void register_pcntl_symbols(int module_number) #endif } +static zend_class_entry *register_class_Pcntl_SignalReturn(void) +{ + zend_class_entry *class_entry = zend_register_internal_enum("Pcntl\\SignalReturn", IS_UNDEF, NULL); + + zend_enum_add_case_cstr(class_entry, "Default", NULL); + + zend_enum_add_case_cstr(class_entry, "Interrupt", NULL); + + return class_entry; +} + static zend_class_entry *register_class_Pcntl_QosClass(void) { zend_class_entry *class_entry = zend_register_internal_enum("Pcntl\\QosClass", IS_UNDEF, NULL); diff --git a/ext/pcntl/pcntl_decl.h b/ext/pcntl/pcntl_decl.h index 7f8e5172cedb..8efc8f4d69ca 100644 --- a/ext/pcntl/pcntl_decl.h +++ b/ext/pcntl/pcntl_decl.h @@ -1,8 +1,13 @@ /* This is a generated file, edit pcntl.stub.php instead. - * Stub hash: 04e7b30c6fb23cf6ce6bc26fe094fd5b4dbfe826 */ + * Stub hash: 2be6f0046170cad02be1d01227b749412af7b51f */ -#ifndef ZEND_PCNTL_DECL_04e7b30c6fb23cf6ce6bc26fe094fd5b4dbfe826_H -#define ZEND_PCNTL_DECL_04e7b30c6fb23cf6ce6bc26fe094fd5b4dbfe826_H +#ifndef ZEND_PCNTL_DECL_2be6f0046170cad02be1d01227b749412af7b51f_H +#define ZEND_PCNTL_DECL_2be6f0046170cad02be1d01227b749412af7b51f_H + +typedef enum zend_enum_Pcntl_SignalReturn { + ZEND_ENUM_Pcntl_SignalReturn_Default = 1, + ZEND_ENUM_Pcntl_SignalReturn_Interrupt = 2, +} zend_enum_Pcntl_SignalReturn; typedef enum zend_enum_Pcntl_QosClass { ZEND_ENUM_Pcntl_QosClass_UserInteractive = 1, @@ -12,4 +17,4 @@ typedef enum zend_enum_Pcntl_QosClass { ZEND_ENUM_Pcntl_QosClass_Background = 5, } zend_enum_Pcntl_QosClass; -#endif /* ZEND_PCNTL_DECL_04e7b30c6fb23cf6ce6bc26fe094fd5b4dbfe826_H */ +#endif /* ZEND_PCNTL_DECL_2be6f0046170cad02be1d01227b749412af7b51f_H */ diff --git a/ext/pcntl/php_pcntl.h b/ext/pcntl/php_pcntl.h index aaf1ea0ad473..64d2e0ac6824 100644 --- a/ext/pcntl/php_pcntl.h +++ b/ext/pcntl/php_pcntl.h @@ -15,6 +15,7 @@ #ifndef PHP_PCNTL_H #define PHP_PCNTL_H +#include "Zend/zend_bitset.h" #include "pcntl_decl.h" #if defined(HAVE_DECL_WCONTINUED) && HAVE_DECL_WCONTINUED == 1 && defined(HAVE_WIFCONTINUED) @@ -48,6 +49,7 @@ ZEND_BEGIN_MODULE_GLOBALS(pcntl) uint16_t num_signals; int last_error; struct php_pcntl_pending_signal *head, *tail, *spares; + zend_bitset restart_syscalls; ZEND_END_MODULE_GLOBALS(pcntl) #if defined(ZTS) && defined(COMPILE_DL_PCNTL) diff --git a/ext/pcntl/tests/signal_return_interrupt_pending.phpt b/ext/pcntl/tests/signal_return_interrupt_pending.phpt new file mode 100644 index 000000000000..cbc795d5de57 --- /dev/null +++ b/ext/pcntl/tests/signal_return_interrupt_pending.phpt @@ -0,0 +1,38 @@ +--TEST-- +SignalReturn::Interrupt with pending signal interrupts socket read before poll +--EXTENSIONS-- +pcntl +posix +--SKIPIF-- + +--FILE-- + +--EXPECT-- +handler +bool(false) diff --git a/ext/pcntl/tests/signal_return_interrupt_syscall.phpt b/ext/pcntl/tests/signal_return_interrupt_syscall.phpt new file mode 100644 index 000000000000..cb78a6133bb5 --- /dev/null +++ b/ext/pcntl/tests/signal_return_interrupt_syscall.phpt @@ -0,0 +1,50 @@ +--TEST-- +SignalReturn::Default with restart_syscalls=false interrupts a blocking socket read +--EXTENSIONS-- +pcntl +posix +--SKIPIF-- + +--FILE-- + +--EXPECT-- +handler +bool(false) +bool(true) diff --git a/ext/pcntl/tests/signal_return_restart_syscall.phpt b/ext/pcntl/tests/signal_return_restart_syscall.phpt new file mode 100644 index 000000000000..9f51c6b86e48 --- /dev/null +++ b/ext/pcntl/tests/signal_return_restart_syscall.phpt @@ -0,0 +1,50 @@ +--TEST-- +SignalReturn::Default with restart_syscalls=true restarts a blocking socket read +--EXTENSIONS-- +pcntl +posix +--SKIPIF-- + +--FILE-- + +--EXPECT-- +handler +string(6) "hello +" diff --git a/main/network.c b/main/network.c index b4d3ded15196..f3bd00004cd1 100644 --- a/main/network.c +++ b/main/network.c @@ -16,6 +16,7 @@ /*#define DEBUG_MAIN_NETWORK 1*/ #include "php.h" +#include "php_deadline.h" #include #include @@ -327,7 +328,8 @@ static inline void php_network_set_limit_time(struct timeval *limit_time, * enable non-blocking mode on the socket. * */ /* {{{ php_network_connect_socket */ -PHPAPI int php_network_connect_socket(php_socket_t sockfd, +PHPAPI int php_network_connect_socket(php_stream *stream, + php_socket_t sockfd, const struct sockaddr *addr, socklen_t addrlen, int asynchronous, @@ -343,6 +345,7 @@ PHPAPI int php_network_connect_socket(php_socket_t sockfd, SET_SOCKET_BLOCKING_MODE(sockfd, orig_flags); + /* Non-blocking: no EINTR handling */ if ((n = connect(sockfd, addr, addrlen)) != 0) { error = php_socket_errno(); @@ -378,49 +381,20 @@ PHPAPI int php_network_connect_socket(php_socket_t sockfd, #else int events = PHP_POLLREADABLE|POLLOUT; #endif - struct timeval working_timeout; -#ifdef HAVE_GETTIMEOFDAY - struct timeval limit_time, time_now; -#endif - if (timeout) { - memcpy(&working_timeout, timeout, sizeof(working_timeout)); -#ifdef HAVE_GETTIMEOFDAY - php_network_set_limit_time(&limit_time, &working_timeout); -#endif - } - - while (true) { - n = php_pollfd_for(sockfd, events, timeout ? &working_timeout : NULL); - if (n < 0) { - if (errno == EINTR) { -#ifdef HAVE_GETTIMEOFDAY - if (timeout) { - gettimeofday(&time_now, NULL); - - if (!timercmp(&time_now, &limit_time, <)) { - /* time limit expired; no need for another poll */ - error = PHP_TIMEOUT_ERROR_VALUE; - break; - } else { - /* work out remaining time */ - sub_times(limit_time, time_now, &working_timeout); - } - } -#endif - continue; - } + php_deadline deadline; + php_deadline_init(&deadline, timeout); + n = php_pollfd_deadline(stream, sockfd, events, &deadline); + if (n < 0) { + ret = -1; + } else if (n == 0) { + error = PHP_TIMEOUT_ERROR_VALUE; + } else { + len = sizeof(error); + /* BSD-derived systems set errno correctly. + * Solaris returns -1 from getsockopt in case of error. */ + if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, (char*)&error, &len) != 0) { ret = -1; - } else if (n == 0) { - error = PHP_TIMEOUT_ERROR_VALUE; - } else { - len = sizeof(error); - /* BSD-derived systems set errno correctly. - * Solaris returns -1 from getsockopt in case of error. */ - if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, (char*)&error, &len) != 0) { - ret = -1; - } } - break; } ok: @@ -883,7 +857,7 @@ PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock, * enable non-blocking mode on the socket. * Returns the connected (or connecting) socket, or -1 on failure. * */ -php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned short port, +php_socket_t php_network_connect_socket_to_host_ex(php_stream *stream, const char *host, unsigned short port, int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string, int *error_code, const char *bindto, unsigned short bindport, long sockopts, php_sockvals *sockvals ) @@ -1043,7 +1017,7 @@ php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned sh #endif } - n = php_network_connect_socket(sock, sa, socklen, asynchronous, + n = php_network_connect_socket(stream, sock, sa, socklen, asynchronous, timeout ? &working_timeout : NULL, error_string, error_code); @@ -1095,7 +1069,7 @@ php_socket_t php_network_connect_socket_to_host(const char *host, unsigned short int *error_code, const char *bindto, unsigned short bindport, long sockopts ) { - return php_network_connect_socket_to_host_ex(host, port, socktype, asynchronous, timeout, + return php_network_connect_socket_to_host_ex(NULL, host, port, socktype, asynchronous, timeout, error_string, error_code, bindto, bindport, sockopts, NULL); } diff --git a/main/php_deadline.h b/main/php_deadline.h new file mode 100644 index 000000000000..a9c68156f3ce --- /dev/null +++ b/main/php_deadline.h @@ -0,0 +1,92 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + */ + +#ifndef PHP_DEADLINE_H +#define PHP_DEADLINE_H + +#include + +#include +#include +#include "Zend/zend_hrtime.h" + +typedef struct php_deadline { + zend_hrtime_t hrtime; +} php_deadline; + +static inline bool php_deadline_is_infinite(php_deadline *deadline) +{ + return deadline->hrtime == (zend_hrtime_t)-1; +} + +/* Initialize an infinite deadline. */ +static inline void php_deadline_init_infinite(php_deadline *deadline) +{ + deadline->hrtime = (zend_hrtime_t)-1; +} + +/* Initialize an non-blocking deadline. */ +static inline void php_deadline_init_nonblock(php_deadline *deadline) +{ + deadline->hrtime = 0; +} + +/* Initialize deadline from a timeout relative to now. */ +static inline void php_deadline_init(php_deadline *deadline, struct timeval *timeout) +{ + if (timeout == NULL || timeout->tv_sec == -1) { + php_deadline_init_infinite(deadline); + return; + } + + ZEND_ASSERT(timeout->tv_sec >= 0); + ZEND_ASSERT(timeout->tv_usec >= 0 && timeout->tv_usec <= 999999); + + deadline->hrtime = zend_hrtime(); + + if (UNEXPECTED((zend_hrtime_t)timeout->tv_sec + >= (ZEND_HRTIME_T_MAX - deadline->hrtime) / ZEND_NANO_IN_SEC)) { + php_deadline_init_infinite(deadline); + return; + } + + deadline->hrtime += timeout->tv_sec * ZEND_NANO_IN_SEC + timeout->tv_usec * 1000; +} + +/* Compute the remaining time until the deadline, in milliseconds, suitable + * for passing directly to poll(2). + * Returns -1 for an infinite deadline or if the remaining time overflows int. + * Returns 0 if the deadline has already passed (non-blocking poll). */ +static inline int php_deadline_to_timeout_ms(php_deadline *deadline) +{ + if (deadline == NULL || php_deadline_is_infinite(deadline)) { + return -1; + } + + zend_hrtime_t now = zend_hrtime(); + + if (deadline->hrtime <= now) { + /* Deadline expired */ + return 0; + } + + zend_hrtime_t remaining = deadline->hrtime - now; + const int nano_in_milli = 1000*1000; + + if (UNEXPECTED(remaining >= (zend_hrtime_t)INT_MAX * nano_in_milli)) { + return -1; + } + + return (remaining + nano_in_milli - 1) / nano_in_milli; +} + +#endif /* PHP_DEADLINE_H */ diff --git a/main/php_network.h b/main/php_network.h index e6d3009a6c82..b5c9043c2ee5 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -16,6 +16,7 @@ #define _PHP_NETWORK_H #include +#include "php_deadline.h" #ifndef PHP_WIN32 # undef closesocket @@ -54,8 +55,10 @@ #ifdef PHP_WIN32 #define php_socket_errno() WSAGetLastError() +#define php_socket_set_errno(err) WSASetLastError(err) #else #define php_socket_errno() errno +#define php_socket_set_errno(err) do { errno = (err); } while (0) #endif /* like strerror, but caller must efree the returned string, @@ -212,6 +215,28 @@ static inline int php_pollfd_for_ms(php_socket_t fd, int events, int timeout) return n; } +static inline int php_pollfd_deadline(php_stream *stream, php_socket_t fd, int events, php_deadline *deadline) +{ + while (1) { + int timeout_ms = php_deadline_to_timeout_ms(deadline); + int n = php_pollfd_for_ms(fd, events, timeout_ms); + + if (n >= 0) { + return n; + } + + if (php_socket_errno() != EINTR) { + return n; + } + + if (php_stream_check_signals(stream) != ZEND_SIGNAL_RESTART) { + /* errno may have been clobbered */ + php_socket_set_errno(EINTR); + return -1; + } + } +} + /* emit warning and suggestion for unsafe select(2) usage */ PHPAPI void _php_emit_fd_setsize_warning(int max_fd); @@ -286,7 +311,7 @@ BEGIN_EXTERN_C() PHPAPI int php_network_getaddresses(const char *host, int socktype, struct sockaddr ***sal, zend_string **error_string); PHPAPI void php_network_freeaddresses(struct sockaddr **sal); -PHPAPI php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned short port, +PHPAPI php_socket_t php_network_connect_socket_to_host_ex(php_stream *stream, const char *host, unsigned short port, int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string, int *error_code, const char *bindto, unsigned short bindport, long sockopts, php_sockvals *sockvals ); @@ -296,7 +321,8 @@ PHPAPI php_socket_t php_network_connect_socket_to_host(const char *host, unsigne int *error_code, const char *bindto, unsigned short bindport, long sockopts ); -PHPAPI int php_network_connect_socket(php_socket_t sockfd, +PHPAPI int php_network_connect_socket(php_stream *stream, + php_socket_t sockfd, const struct sockaddr *addr, socklen_t addrlen, int asynchronous, @@ -305,7 +331,7 @@ PHPAPI int php_network_connect_socket(php_socket_t sockfd, int *error_code); #define php_connect_nonb(sock, addr, addrlen, timeout) \ - php_network_connect_socket((sock), (addr), (addrlen), 0, (timeout), NULL, NULL) + php_network_connect_socket(NULL, (sock), (addr), (addrlen), 0, (timeout), NULL, NULL) PHPAPI php_socket_t php_network_bind_socket_to_local_addr_ex(const char *host, unsigned port, int socktype, long sockopts, php_sockvals *sockvals, zend_string **error_string, int *error_code diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index 3eef731544de..75f2aaa20273 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -16,6 +16,8 @@ #include "ext/standard/file.h" #include "php_streams.h" #include "php_io.h" +#include "php_network.h" +#include "php_deadline.h" #if defined(PHP_WIN32) || defined(__riscos__) # undef AF_UNIX @@ -66,45 +68,46 @@ static ssize_t php_sockop_write(php_stream *stream, const char *buf, size_t coun { php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract; ssize_t didwrite; - struct timeval *ptimeout; if (!sock || sock->socket == -1) { return 0; } - if (sock->timeout.tv_sec == -1) - ptimeout = NULL; - else - ptimeout = &sock->timeout; + /* Compute deadline once so that signal-restart loops don't extend the timeout. */ + php_deadline deadline; + php_deadline_init(&deadline, &sock->timeout); + bool finite_timeout = !php_deadline_is_infinite(&deadline); retry: - didwrite = send(sock->socket, buf, XP_SOCK_BUF_SIZE(count), (sock->is_blocked && ptimeout) ? MSG_DONTWAIT : 0); + if (php_stream_check_signals(stream) == ZEND_SIGNAL_INTERRUPT) { + return -1; + } + + didwrite = send(sock->socket, buf, XP_SOCK_BUF_SIZE(count), (sock->is_blocked && finite_timeout) ? MSG_DONTWAIT : 0); if (didwrite <= 0) { char *estr; int err = php_socket_errno(); + if (err == EINTR) { + goto retry; + } + if (PHP_IS_TRANSIENT_ERROR(err)) { if (sock->is_blocked) { int retval; sock->timeout_event = false; - do { - retval = php_pollfd_for(sock->socket, POLLOUT, ptimeout); - - if (retval == 0) { - sock->timeout_event = true; - break; - } + retval = php_pollfd_deadline(stream, sock->socket, POLLOUT, &deadline); - if (retval > 0) { - /* writable now; retry */ - goto retry; - } + if (retval == 0) { + sock->timeout_event = true; + } else if (retval > 0) { + goto retry; + } - err = php_socket_errno(); - } while (err == EINTR); + err = php_socket_errno(); } else { /* EWOULDBLOCK/EAGAIN is not an error for a non-blocking stream. * Report zero byte write instead. */ @@ -112,7 +115,7 @@ static ssize_t php_sockop_write(php_stream *stream, const char *buf, size_t coun } } - if (!(stream->flags & PHP_STREAM_FLAG_SUPPRESS_ERRORS)) { + if (!(stream->flags & PHP_STREAM_FLAG_SUPPRESS_ERRORS) && err != EINTR) { estr = php_socket_strerror(err, NULL, 0); php_stream_warn(stream, NetworkSendFailed, "Send of %zu bytes failed with errno=%d %s", count, err, estr); @@ -127,40 +130,31 @@ static ssize_t php_sockop_write(php_stream *stream, const char *buf, size_t coun return didwrite; } -static void php_sock_stream_wait_for_data(php_stream *stream, php_netstream_data_t *sock, bool has_buffered_data) +static zend_result php_sock_stream_wait_for_data(php_stream *stream, php_netstream_data_t *sock, bool has_buffered_data, php_deadline *deadline) { int retval; - struct timeval *ptimeout, zero_timeout; if (!sock || sock->socket == -1) { - return; + return FAILURE; } sock->timeout_event = false; + php_deadline nonblock; if (has_buffered_data) { - /* If there is already buffered data, use no timeout. */ - zero_timeout.tv_sec = 0; - zero_timeout.tv_usec = 0; - ptimeout = &zero_timeout; - } else if (sock->timeout.tv_sec == -1) { - ptimeout = NULL; - } else { - ptimeout = &sock->timeout; + /* If there is already buffered data, do not block. */ + php_deadline_init_nonblock(&nonblock); + deadline = &nonblock; } - while(1) { - retval = php_pollfd_for(sock->socket, PHP_POLLREADABLE, ptimeout); - - if (retval == 0) - sock->timeout_event = true; - - if (retval >= 0) - break; - - if (php_socket_errno() != EINTR) - break; + retval = php_pollfd_deadline(stream, sock->socket, PHP_POLLREADABLE, deadline); + if (retval == 0) { + sock->timeout_event = true; + } else if (retval < 0 && php_socket_errno() == EINTR) { + return FAILURE; } + + return SUCCESS; } static ssize_t php_sockop_read(php_stream *stream, char *buf, size_t count) @@ -171,6 +165,17 @@ static ssize_t php_sockop_read(php_stream *stream, char *buf, size_t count) return -1; } + /* Compute deadline once so that signal-restart loops don't extend the timeout. */ + php_deadline deadline; + if (sock->is_blocked) { + php_deadline_init(&deadline, &sock->timeout); + } + +restart: + if (zend_signal_interrupt_function() == ZEND_SIGNAL_INTERRUPT) { + return -1; + } + int recv_flags = 0; /* Special handling for blocking read. */ if (sock->is_blocked) { @@ -187,11 +192,14 @@ static ssize_t php_sockop_read(php_stream *stream, char *buf, size_t count) /* If the wait is needed or it is a platform without MSG_DONTWAIT support (e.g. Windows), * then poll for data. */ if (!dont_wait || MSG_DONTWAIT == 0) { - php_sock_stream_wait_for_data(stream, sock, has_buffered_data); + zend_result wait_result = php_sock_stream_wait_for_data(stream, sock, has_buffered_data, &deadline); if (sock->timeout_event) { /* It is ok to timeout if there is any data buffered so return 0, otherwise -1. */ return has_buffered_data ? 0 : -1; } + if (wait_result != SUCCESS) { + return -1; + } } } @@ -199,7 +207,9 @@ static ssize_t php_sockop_read(php_stream *stream, char *buf, size_t count) int err = php_socket_errno(); if (nr_bytes < 0) { - if (PHP_IS_TRANSIENT_ERROR(err)) { + if (err == EINTR) { + goto restart; + } else if (PHP_IS_TRANSIENT_ERROR(err)) { nr_bytes = 0; } else { stream->eof = 1; @@ -219,9 +229,6 @@ static ssize_t php_sockop_read(php_stream *stream, char *buf, size_t count) static int php_sockop_close(php_stream *stream, int close_handle) { php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract; -#ifdef PHP_WIN32 - int n; -#endif if (!sock) { return 0; @@ -244,9 +251,10 @@ static int php_sockop_close(php_stream *stream, int close_handle) * We use a small timeout which should encourage the OS to send the data, * but at the same time avoid hanging indefinitely. * */ - do { - n = php_pollfd_for_ms(sock->socket, POLLOUT, 500); - } while (n == -1 && php_socket_errno() == EINTR); + php_deadline deadline; + struct timeval timeout = {.tv_sec = 0, .tv_usec = 500000}; + php_deadline_init(&deadline, &timeout); + php_pollfd_deadline(stream, sock->socket, POLLOUT, &deadline); #endif closesocket(sock->socket); sock->socket = SOCK_ERR; @@ -279,24 +287,49 @@ static int php_sockop_stat(php_stream *stream, php_stream_statbuf *ssb) #endif } -static inline int sock_sendto(php_netstream_data_t *sock, const char *buf, size_t buflen, int flags, +static inline int sock_sendto(php_stream *stream, php_netstream_data_t *sock, const char *buf, size_t buflen, int flags, struct sockaddr *addr, socklen_t addrlen ) { int ret; if (addr) { - ret = sendto(sock->socket, buf, XP_SOCK_BUF_SIZE(buflen), flags, addr, XP_SOCK_BUF_SIZE(addrlen)); + while (1) { + ret = sendto(sock->socket, buf, XP_SOCK_BUF_SIZE(buflen), flags, addr, XP_SOCK_BUF_SIZE(addrlen)); + + if (ret >= 0 || php_socket_errno() != EINTR) { + break; + } + + if (php_stream_check_signals(stream) == ZEND_SIGNAL_INTERRUPT) { + php_socket_set_errno(EINTR); + break; + } + } return (ret == SOCK_CONN_ERR) ? -1 : ret; } + + while (1) { #ifdef PHP_WIN32 - return ((ret = send(sock->socket, buf, buflen > INT_MAX ? INT_MAX : (int)buflen, flags)) == SOCK_CONN_ERR) ? -1 : ret; + ret = send(sock->socket, buf, buflen > INT_MAX ? INT_MAX : (int)buflen, flags); #else - return ((ret = send(sock->socket, buf, buflen, flags)) == SOCK_CONN_ERR) ? -1 : ret; + ret = send(sock->socket, buf, buflen, flags); #endif + + if (ret >= 0 || php_socket_errno() != EINTR) { + break; + } + + if (php_stream_check_signals(stream) == ZEND_SIGNAL_INTERRUPT) { + php_socket_set_errno(EINTR); + break; + } + } + + return (ret == SOCK_CONN_ERR) ? -1 : ret; } -static inline int sock_recvfrom(php_netstream_data_t *sock, char *buf, size_t buflen, int flags, +static inline int sock_recvfrom(php_stream *stream, php_netstream_data_t *sock, char *buf, size_t buflen, int flags, zend_string **textaddr, struct sockaddr **addr, socklen_t *addrlen ) @@ -306,15 +339,28 @@ static inline int sock_recvfrom(php_netstream_data_t *sock, char *buf, size_t bu if (want_addr) { php_sockaddr_storage sa; - socklen_t sl = sizeof(sa); - ret = recvfrom(sock->socket, buf, XP_SOCK_BUF_SIZE(buflen), flags, (struct sockaddr*)&sa, &sl); - ret = (ret == SOCK_CONN_ERR) ? -1 : ret; + socklen_t sl; + + while (1) { + sl = sizeof(sa); + ret = recvfrom(sock->socket, buf, XP_SOCK_BUF_SIZE(buflen), flags, (struct sockaddr*)&sa, &sl); + ret = (ret == SOCK_CONN_ERR) ? -1 : ret; #ifdef PHP_WIN32 - /* POSIX discards excess bytes without signalling failure; emulate this on Windows */ - if (ret == -1 && WSAGetLastError() == WSAEMSGSIZE) { - ret = buflen; - } + /* POSIX discards excess bytes without signalling failure; emulate this on Windows */ + if (ret == -1 && WSAGetLastError() == WSAEMSGSIZE) { + ret = buflen; + } #endif + if (ret != -1 || php_socket_errno() != EINTR) { + break; + } + + if (php_stream_check_signals(stream) == ZEND_SIGNAL_INTERRUPT) { + php_socket_set_errno(EINTR); + break; + } + } + if (sl) { php_network_populate_name_from_sockaddr((struct sockaddr*)&sa, sl, textaddr, addr, addrlen); @@ -328,8 +374,19 @@ static inline int sock_recvfrom(php_netstream_data_t *sock, char *buf, size_t bu } } } else { - ret = recv(sock->socket, buf, XP_SOCK_BUF_SIZE(buflen), flags); - ret = (ret == SOCK_CONN_ERR) ? -1 : ret; + while (1) { + ret = recv(sock->socket, buf, XP_SOCK_BUF_SIZE(buflen), flags); + ret = (ret == SOCK_CONN_ERR) ? -1 : ret; + + if (ret != -1 || php_socket_errno() != EINTR) { + break; + } + + if (php_stream_check_signals(stream) == ZEND_SIGNAL_INTERRUPT) { + php_socket_set_errno(EINTR); + break; + } + } } return ret; @@ -351,6 +408,7 @@ static int php_sockop_set_option(php_stream *stream, int option, int value, void struct timeval tv; char buf; int alive = 1; + php_deadline deadline; if (value == -1) { if (sock->timeout.tv_sec == -1) { @@ -364,6 +422,8 @@ static int php_sockop_set_option(php_stream *stream, int option, int value, void tv.tv_usec = 0; } + php_deadline_init(&deadline, &tv); + if (sock->socket == -1) { alive = 0; } else if ( @@ -372,7 +432,7 @@ static int php_sockop_set_option(php_stream *stream, int option, int value, void !(stream->flags & PHP_STREAM_FLAG_NO_IO) && ((MSG_DONTWAIT != 0) || !sock->is_blocked) ) || - php_pollfd_for(sock->socket, PHP_POLLREADABLE|POLLPRI, &tv) > 0 + php_pollfd_deadline(stream, sock->socket, PHP_POLLREADABLE|POLLPRI, &deadline) > 0 ) { /* the poll() call was skipped if the socket is non-blocking (or MSG_DONTWAIT is available) and if the timeout is zero */ #ifdef PHP_WIN32 @@ -381,6 +441,7 @@ static int php_sockop_set_option(php_stream *stream, int option, int value, void ssize_t ret; #endif + /* Non-blocking: no EINTR handling */ ret = recv(sock->socket, &buf, sizeof(buf), MSG_PEEK|MSG_DONTWAIT); if (0 == ret) { /* the counterpart did properly shutdown */ @@ -444,7 +505,7 @@ static int php_sockop_set_option(php_stream *stream, int option, int value, void if ((xparam->inputs.flags & STREAM_OOB) == STREAM_OOB) { flags |= MSG_OOB; } - xparam->outputs.returncode = sock_sendto(sock, + xparam->outputs.returncode = sock_sendto(stream, sock, xparam->inputs.buf, xparam->inputs.buflen, flags, xparam->inputs.addr, @@ -464,7 +525,7 @@ static int php_sockop_set_option(php_stream *stream, int option, int value, void if ((xparam->inputs.flags & STREAM_PEEK) == STREAM_PEEK) { flags |= MSG_PEEK; } - xparam->outputs.returncode = sock_recvfrom(sock, + xparam->outputs.returncode = sock_recvfrom(stream, sock, xparam->inputs.buf, xparam->inputs.buflen, flags, xparam->want_textaddr ? &xparam->outputs.textaddr : NULL, @@ -840,7 +901,7 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_ parse_unix_address(stream, xparam, &unix_addr); - ret = php_network_connect_socket(sock->socket, + ret = php_network_connect_socket(stream, sock->socket, (const struct sockaddr *)&unix_addr, (socklen_t) offsetof(struct sockaddr_un, sun_path) + xparam->inputs.namelen, xparam->op == STREAM_XPORT_OP_CONNECT_ASYNC, xparam->inputs.timeout, xparam->want_errortext ? &xparam->outputs.error_text : NULL, @@ -931,7 +992,7 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_ * want the default to be TCP sockets so that the openssl extension can * re-use this code. */ - sock->socket = php_network_connect_socket_to_host_ex(host, portno, + sock->socket = php_network_connect_socket_to_host_ex(stream, host, portno, PHP_STREAM_XPORT_IS_UDP(stream) ? SOCK_DGRAM : SOCK_STREAM, xparam->op == STREAM_XPORT_OP_CONNECT_ASYNC, xparam->inputs.timeout, From b658fc90c1625df917a33defabd478107383b570 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 6 Jul 2026 19:17:07 +0200 Subject: [PATCH 2/4] Clamp timeout setting --- ext/pdo_mysql/mysql_driver.c | 3 +++ ext/standard/http_fopen_wrapper.c | 3 +++ 2 files changed, 6 insertions(+) diff --git a/ext/pdo_mysql/mysql_driver.c b/ext/pdo_mysql/mysql_driver.c index 54a8803971ca..d4ad2eb45ac8 100644 --- a/ext/pdo_mysql/mysql_driver.c +++ b/ext/pdo_mysql/mysql_driver.c @@ -749,6 +749,9 @@ static int pdo_mysql_handle_factory(pdo_dbh_t *dbh, zval *driver_options) /* handle MySQL options */ if (driver_options) { zend_long connect_timeout = pdo_attr_lval(driver_options, PDO_ATTR_TIMEOUT, 30); + if (connect_timeout < -1) { + connect_timeout = -1; + } zend_string *init_cmd = NULL; #ifndef PDO_USE_MYSQLND zend_string *default_file = NULL, *default_group = NULL; diff --git a/ext/standard/http_fopen_wrapper.c b/ext/standard/http_fopen_wrapper.c index 4b2aa7116a24..65ead5d6a33a 100644 --- a/ext/standard/http_fopen_wrapper.c +++ b/ext/standard/http_fopen_wrapper.c @@ -482,6 +482,9 @@ static php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper, php_uri_struct_free(resource); return NULL; } + if (d < -1) { + d = -1; + } #ifndef PHP_WIN32 timeout.tv_sec = (time_t) d; timeout.tv_usec = (size_t) ((d - timeout.tv_sec) * 1000000); From 3f4e7b867f01c28f05f5f0b53ca32ae599ef6b48 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 6 Jul 2026 13:50:46 +0200 Subject: [PATCH 3/4] Disallow concurrent access to stream via signal handlers --- .../tests/streams/concurrent_access.phpt | 60 ++++++++++++++++ .../streams/concurrent_access_fatal.phpt | 54 +++++++++++++++ main/php_streams.h | 31 +++++++++ main/streams/streams.c | 68 ++++++++++++++++++- main/streams/xp_socket.c | 32 ++++++++- 5 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 ext/standard/tests/streams/concurrent_access.phpt create mode 100644 ext/standard/tests/streams/concurrent_access_fatal.phpt diff --git a/ext/standard/tests/streams/concurrent_access.phpt b/ext/standard/tests/streams/concurrent_access.phpt new file mode 100644 index 000000000000..6d955fc61230 --- /dev/null +++ b/ext/standard/tests/streams/concurrent_access.phpt @@ -0,0 +1,60 @@ +--TEST-- +Stream cannot be accessed concurrently via signal handlers +--EXTENSIONS-- +pcntl +posix +--SKIPIF-- + +--FILE-- +getMessage(), "\n"; + } + try { + fclose($read); + } catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), "\n"; + } +}, restart_syscalls: true); + +$pid = pcntl_fork(); +if ($pid === 0) { + fwrite($write, "1"); + fclose($read); + usleep(200_000); // let parent block in fread() + posix_kill(posix_getppid(), SIGUSR1); + usleep(200_000); // let parent block in fread() + fwrite($write, "hello\n"); + fclose($write); + exit(0); +} + +// Wait for child to start +fread($read, 1); + +$result = fread($read, 1024); +var_dump($result); + +pcntl_waitpid($pid, $status); +fclose($read); +fclose($write); +?> +--EXPECT-- +handler +Error: Concurrent access to a stream +Error: Concurrent access to a stream +string(6) "hello +" diff --git a/ext/standard/tests/streams/concurrent_access_fatal.phpt b/ext/standard/tests/streams/concurrent_access_fatal.phpt new file mode 100644 index 000000000000..4070bacf14a8 --- /dev/null +++ b/ext/standard/tests/streams/concurrent_access_fatal.phpt @@ -0,0 +1,54 @@ +--TEST-- +Stream is closed while marked in_use after a fatal error +--EXTENSIONS-- +pcntl +posix +--SKIPIF-- + +--FILE-- +getMessage(), "\n"; + return; + } + echo "unreachable (shutdown function)\n"; +}); + +// Wait for child to start +fread($read, 1); + +$result = fread($read, 1024); + +echo "unreachable"; +?> +--EXPECTF-- +handler + +Fatal error: Cannot use "bool" as a class name as it is reserved in %s : eval()'d code on line 1 +shutdown function still cannot access stream: Error: Concurrent access to a stream diff --git a/main/php_streams.h b/main/php_streams.h index 0dd27b8ee88a..828e81e496f7 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -189,6 +189,11 @@ struct _php_stream_wrapper { #define PHP_STREAM_FLAG_NO_IO 0x400 +/* Stream is in-use: accessing it would be unsafe. This is set when a stream + * operation is interrupted by a signal, before calling signal handlers. + * Stream ops and public stream APIs check this flag before proceeding. */ +#define PHP_STREAM_FLAG_IN_USE 0x800 + #define PHP_STREAM_FLAG_WAS_WRITTEN 0x80000000 struct _php_stream { @@ -681,6 +686,32 @@ static inline bool php_is_stream_path(const char *filename) return ((p != filename) && (p[0] == ':') && (p[1] == '/') && (p[2] == '/')); } +static inline zend_signal_interrupt_result php_stream_check_signals(php_stream *stream) +{ + if (stream != NULL && zend_signal_interrupt_function != zend_signal_interrupt) { + uint32_t orig_in_use = stream->flags & PHP_STREAM_FLAG_IN_USE; + stream->flags |= PHP_STREAM_FLAG_IN_USE; + zend_signal_interrupt_result result = zend_signal_interrupt_function(); + stream->flags &= ~PHP_STREAM_FLAG_IN_USE; + stream->flags |= orig_in_use; + return result; + } + + return zend_signal_interrupt_function(); +} + +PHPAPI ZEND_COLD void php_stream_in_use_error(void); + +/* See PHP_STREAM_FLAG_IN_USE */ +static inline zend_result php_stream_check_in_use(php_stream *stream) +{ + if (UNEXPECTED(stream->flags & PHP_STREAM_FLAG_IN_USE)) { + php_stream_in_use_error(); + return FAILURE; + } + return SUCCESS; +} + END_EXTERN_C() #endif diff --git a/main/streams/streams.c b/main/streams/streams.c index 3d8830d7291f..6cdaaf52023c 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -257,6 +257,12 @@ PHPAPI int _php_stream_free(php_stream *stream, int close_options) /* {{{ */ int release_cast = 1; php_stream_context *context; + /* Not using php_stream_check_in_use() as we want to allow releasing file descriptors during resource shutdown */ + if (UNEXPECTED((stream->flags & PHP_STREAM_FLAG_IN_USE) && !(EG(flags) & EG_FLAGS_IN_RESOURCE_SHUTDOWN))) { + php_stream_in_use_error(); + return 0; + } + /* During shutdown resources may be released before other resources still holding them. * When only resources are referenced this is not a problem, because they are refcounted * and will only be fully freed once the refcount drops to zero. However, if php_stream* @@ -435,6 +441,10 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size) { /* allocate/fill the buffer */ + if (php_stream_check_in_use(stream) != SUCCESS) { + return FAILURE; + } + zend_result retval; bool old_eof = stream->eof; @@ -607,6 +617,10 @@ PHPAPI ssize_t _php_stream_read(php_stream *stream, char *buf, size_t size) { ssize_t toread = 0, didread = 0; + if (php_stream_check_in_use(stream) != SUCCESS) { + return 0; + } + while (size > 0) { /* take from the read buffer first. @@ -709,6 +723,10 @@ PHPAPI zend_string *php_stream_read_to_str(php_stream *stream, size_t len) PHPAPI bool _php_stream_eof(php_stream *stream) { + if (php_stream_check_in_use(stream) != SUCCESS) { + return 0; + } + /* if there is data in the buffer, it's not EOF */ if (stream->writepos - stream->readpos > 0) { return 0; @@ -758,6 +776,10 @@ PHPAPI bool _php_stream_puts(php_stream *stream, const char *buf) PHPAPI int _php_stream_stat(php_stream *stream, php_stream_statbuf *ssb) { + if (php_stream_check_in_use(stream) != SUCCESS) { + return -1; + } + memset(ssb, 0, sizeof(*ssb)); /* if the stream was wrapped, allow the wrapper to stat it */ @@ -782,6 +804,10 @@ PHPAPI const char *php_stream_locate_eol(php_stream *stream, zend_string *buf) const char *eol = NULL; const char *readptr; + if (php_stream_check_in_use(stream) != SUCCESS) { + return 0; + } + if (!buf) { readptr = (char*)stream->readbuf + stream->readpos; avail = stream->writepos - stream->readpos; @@ -827,6 +853,10 @@ PHPAPI char *_php_stream_get_line(php_stream *stream, char *buf, size_t maxlen, int grow_mode = 0; char *bufstart = buf; + if (php_stream_check_in_use(stream) != SUCCESS) { + return NULL; + } + if (buf == NULL) { grow_mode = 1; } else if (maxlen == 0) { @@ -973,6 +1003,10 @@ PHPAPI zend_string *php_stream_get_record(php_stream *stream, size_t maxlen, con tent_ret_len; /* tentative returned length */ bool has_delim = delim_len > 0; + if (php_stream_check_in_use(stream) != SUCCESS) { + return NULL; + } + if (maxlen == 0) { return NULL; } @@ -1187,6 +1221,10 @@ PHPAPI int _php_stream_flush(php_stream *stream, int closing) { int ret = 0; + if (php_stream_check_in_use(stream) != SUCCESS) { + return -1; + } + if (stream->writefilters.head && stream->ops->write) { _php_stream_write_filtered(stream, NULL, 0, closing ? PSFS_FLAG_FLUSH_CLOSE : PSFS_FLAG_FLUSH_INC ); } @@ -1204,6 +1242,10 @@ PHPAPI ssize_t _php_stream_write(php_stream *stream, const char *buf, size_t cou { ssize_t bytes; + if (php_stream_check_in_use(stream) != SUCCESS) { + return (ssize_t) -1; + } + if (count == 0) { return 0; } @@ -1306,6 +1348,10 @@ static zend_result php_stream_filters_seek_all(php_stream *stream, bool is_start PHPAPI int _php_stream_seek(php_stream *stream, zend_off_t offset, int whence) { + if (php_stream_check_in_use(stream) != SUCCESS) { + return -1; + } + if (stream->fclose_stdiocast == PHP_STREAM_FCLOSE_FOPENCOOKIE) { /* flush can call seek internally so we need to prevent an infinite loop */ if (!stream->fclose_stdiocast_flush_in_progress) { @@ -1415,6 +1461,10 @@ PHPAPI int _php_stream_set_option(php_stream *stream, int option, int value, voi { int ret = PHP_STREAM_OPTION_RETURN_NOTIMPL; + if (php_stream_check_in_use(stream) != SUCCESS) { + return PHP_STREAM_OPTION_RETURN_ERR; + } + if (stream->ops->set_option) { ret = stream->ops->set_option(stream, option, value, ptrparam); } @@ -1465,6 +1515,10 @@ PHPAPI ssize_t _php_stream_passthru(php_stream * stream STREAMS_DC) char buf[8192]; ssize_t b; + if (php_stream_check_in_use(stream) != SUCCESS) { + return -1; + } + if (php_stream_mmap_possible(stream)) { char *p; size_t mapped; @@ -1506,6 +1560,10 @@ PHPAPI zend_string *_php_stream_copy_to_mem(php_stream *src, size_t maxlen, bool php_stream_statbuf ssbuf; zend_string *result; + if (php_stream_check_in_use(src) != SUCCESS) { + return NULL; + } + if (maxlen == 0) { return ZSTR_EMPTY_ALLOC(); } @@ -1594,7 +1652,6 @@ static zend_result php_stream_copy_fallback(php_stream *src, php_stream *dest, s { char buf[CHUNK_SIZE]; size_t haveread = 0; - if (maxlen == PHP_STREAM_COPY_ALL) { maxlen = 0; } @@ -2240,6 +2297,10 @@ PHPAPI php_stream_context *php_stream_context_set(php_stream *stream, php_stream { php_stream_context *oldcontext = PHP_STREAM_CONTEXT(stream); + if (php_stream_check_in_use(stream) != SUCCESS) { + return NULL; + } + if (context) { stream->ctx = context->res; GC_ADDREF(context->res); @@ -2430,3 +2491,8 @@ PHPAPI int _php_stream_scandir(const char *dirname, zend_string **namelist[], in return -1; } /* }}} */ + +PHPAPI ZEND_COLD void php_stream_in_use_error(void) +{ + zend_throw_error(NULL, "Concurrent access to a stream"); +} diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index 75f2aaa20273..017222f33e78 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -69,6 +69,10 @@ static ssize_t php_sockop_write(php_stream *stream, const char *buf, size_t coun php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract; ssize_t didwrite; + if (php_stream_check_in_use(stream) != SUCCESS) { + return -1; + } + if (!sock || sock->socket == -1) { return 0; } @@ -161,6 +165,10 @@ static ssize_t php_sockop_read(php_stream *stream, char *buf, size_t count) { php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract; + if (php_stream_check_in_use(stream) != SUCCESS) { + return -1; + } + if (!sock || sock->socket == -1) { return -1; } @@ -172,7 +180,7 @@ static ssize_t php_sockop_read(php_stream *stream, char *buf, size_t count) } restart: - if (zend_signal_interrupt_function() == ZEND_SIGNAL_INTERRUPT) { + if (php_stream_check_signals(stream) == ZEND_SIGNAL_INTERRUPT) { return -1; } @@ -230,6 +238,12 @@ static int php_sockop_close(php_stream *stream, int close_handle) { php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract; + /* Not using php_stream_check_in_use() as we want to allow releasing file descriptors during resource shutdown */ + if (UNEXPECTED((stream->flags & PHP_STREAM_FLAG_IN_USE) && !(EG(flags) & EG_FLAGS_IN_RESOURCE_SHUTDOWN))) { + php_stream_in_use_error(); + return -1; + } + if (!sock) { return 0; } @@ -270,6 +284,10 @@ static int php_sockop_close(php_stream *stream, int close_handle) static int php_sockop_flush(php_stream *stream) { #if 0 + if (php_stream_check_in_use(stream) != SUCCESS) { + return -1; + } + php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract; return fsync(sock->socket); #endif @@ -283,6 +301,10 @@ static int php_sockop_stat(php_stream *stream, php_stream_statbuf *ssb) #else php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract; + if (php_stream_check_in_use(stream) != SUCCESS) { + return -1; + } + return zend_fstat(sock->socket, &ssb->sb); #endif } @@ -565,6 +587,10 @@ static int php_sockop_cast(php_stream *stream, int castas, void **ret) { php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract; + if (php_stream_check_in_use(stream) != SUCCESS) { + return FAILURE; + } + if (!sock) { return FAILURE; } @@ -1083,6 +1109,10 @@ static int php_tcp_sockop_set_option(php_stream *stream, int option, int value, php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract; php_stream_xport_param *xparam; + if (php_stream_check_in_use(stream) != SUCCESS) { + return -1; + } + switch(option) { case PHP_STREAM_OPTION_XPORT_API: xparam = (php_stream_xport_param *)ptrparam; From 580c5ab75b2ef00deb01b4cee570128375e6edbe Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 6 Jul 2026 16:15:08 +0200 Subject: [PATCH 4/4] Allow nested signal handling This makes it possible to interrupt syscalls performed by signal handlers. Refactor the queue to a ring buffer as it's more concurrency friendly. --- ext/pcntl/pcntl.c | 130 +++++++----------- ext/pcntl/php_pcntl.h | 6 +- ext/pcntl/tests/signal_nested_delivery.phpt | 38 +++++ .../tests/signal_nested_delivery_io.phpt | 47 +++++++ 4 files changed, 141 insertions(+), 80 deletions(-) create mode 100644 ext/pcntl/tests/signal_nested_delivery.phpt create mode 100644 ext/pcntl/tests/signal_nested_delivery_io.phpt diff --git a/ext/pcntl/pcntl.c b/ext/pcntl/pcntl.c index 63a732adb202..c4df24907dfa 100644 --- a/ext/pcntl/pcntl.c +++ b/ext/pcntl/pcntl.c @@ -207,7 +207,6 @@ PHP_RINIT_FUNCTION(pcntl) { php_add_tick_function(pcntl_signal_dispatch_tick_function, NULL); zend_hash_init(&PCNTL_G(php_signal_table), 16, NULL, ZVAL_PTR_DTOR, 0); - PCNTL_G(head) = PCNTL_G(tail) = PCNTL_G(spares) = NULL; PCNTL_G(async_signals) = 0; PCNTL_G(last_error) = 0; PCNTL_G(num_signals) = NSIG; @@ -237,7 +236,6 @@ PHP_MINIT_FUNCTION(pcntl) PHP_RSHUTDOWN_FUNCTION(pcntl) { - struct php_pcntl_pending_signal *sig; zend_long signo; zval *handle; @@ -250,15 +248,10 @@ PHP_RSHUTDOWN_FUNCTION(pcntl) zend_hash_destroy(&PCNTL_G(php_signal_table)); - while (PCNTL_G(head)) { - sig = PCNTL_G(head); - PCNTL_G(head) = sig->next; - efree(sig); - } - while (PCNTL_G(spares)) { - sig = PCNTL_G(spares); - PCNTL_G(spares) = sig->next; - efree(sig); + if (PCNTL_G(pending_signals_queue)) { + efree(PCNTL_G(pending_signals_queue)); + PCNTL_G(pending_signals_queue) = NULL; + PCNTL_G(pending_signals) = false; } efree(PCNTL_G(restart_syscalls)); @@ -820,16 +813,10 @@ PHP_FUNCTION(pcntl_signal) RETURN_THROWS(); } - if (!PCNTL_G(spares)) { - /* since calling malloc() from within a signal handler is not portable, - * pre-allocate a few records for recording signals */ - for (unsigned int i = 0; i < PCNTL_G(num_signals); i++) { - struct php_pcntl_pending_signal *psig; - - psig = emalloc(sizeof(*psig)); - psig->next = PCNTL_G(spares); - PCNTL_G(spares) = psig; - } + if (!PCNTL_G(pending_signals_queue)) { + zend_atomic_int_store_ex(&PCNTL_G(pending_signals_head), 0); + zend_atomic_int_store_ex(&PCNTL_G(pending_signals_tail), 0); + PCNTL_G(pending_signals_queue) = safe_emalloc(PCNTL_G(num_signals), sizeof(struct php_pcntl_pending_signal), 0); } /* If restart_syscalls was not explicitly specified and the signal is SIGALRM, then default @@ -1344,71 +1331,72 @@ PHP_FUNCTION(pcntl_strerror) /* Our custom signal handler that calls the appropriate php_function */ static void pcntl_signal_handler(int signo, siginfo_t *siginfo, void *context) { - struct php_pcntl_pending_signal *psig = PCNTL_G(spares); - if (!psig) { + /* Only pcntl_signal_handler() modifies pending_signals_tail. + * Signals are masked during execution. */ + + int tail = zend_atomic_int_load_ex(&PCNTL_G(pending_signals_tail)); + int next_tail = (tail + 1) % PCNTL_G(num_signals); + + if (next_tail == zend_atomic_int_load_ex(&PCNTL_G(pending_signals_head))) { /* oops, too many signals for us to track, so we'll forget about this one */ return; } - PCNTL_G(spares) = psig->next; - psig->signo = signo; - psig->next = NULL; + struct php_pcntl_pending_signal *psig = &PCNTL_G(pending_signals_queue)[tail]; + psig->signo = signo; psig->siginfo = *siginfo; - /* the head check is important, as the tick handler cannot atomically clear both - * the head and tail */ - if (PCNTL_G(head) && PCNTL_G(tail)) { - PCNTL_G(tail)->next = psig; - } else { - PCNTL_G(head) = psig; - } - PCNTL_G(tail) = psig; + zend_atomic_int_store_ex(&PCNTL_G(pending_signals_tail), next_tail); + PCNTL_G(pending_signals) = true; if (PCNTL_G(async_signals)) { zend_atomic_bool_store_ex(&EG(vm_interrupt), true); } } +static bool pcntl_signal_dequeue(struct php_pcntl_pending_signal *sig) +{ + /* Only pcntl_signal_dequeue() modifies pending_signals_head. + * pcntl_signal_dequeue() is never called concurrently with itself. */ + + int head = zend_atomic_int_load_ex(&PCNTL_G(pending_signals_head)); + + if (head == zend_atomic_int_load_ex(&PCNTL_G(pending_signals_tail))) { + return false; /* Queue is empty */ + } + + *sig = PCNTL_G(pending_signals_queue)[head]; + + zend_atomic_int_store_ex(&PCNTL_G(pending_signals_head), (head + 1) % PCNTL_G(num_signals)); + + return true; +} + zend_signal_interrupt_result pcntl_signal_dispatch(void) { zval params[2], *handle, retval; - struct php_pcntl_pending_signal *queue, *next; - sigset_t mask; - sigset_t old_mask; + struct php_pcntl_pending_signal sig; bool interrupt = false; if(!PCNTL_G(pending_signals)) { return ZEND_SIGNAL_RESTART; } - /* Mask all signals */ - sigfillset(&mask); - sigprocmask(SIG_BLOCK, &mask, &old_mask); - - /* Bail if the queue is empty or if we are already playing the queue */ - // TODO: For the purpose of EINTR handling, processing next signals here would be useful - if (!PCNTL_G(head) || PCNTL_G(processing_signal_queue)) { - sigprocmask(SIG_SETMASK, &old_mask, NULL); - return ZEND_SIGNAL_RESTART; - } + PCNTL_G(pending_signals) = false; /* Prevent switching fibers when handling signals */ zend_fiber_switch_block(); - /* Prevent reentrant handler calls */ - PCNTL_G(processing_signal_queue) = true; - - queue = PCNTL_G(head); - PCNTL_G(head) = NULL; /* simple stores are atomic */ - PCNTL_G(tail) = NULL; - - while (queue) { - if ((handle = zend_hash_index_find(&PCNTL_G(php_signal_table), queue->signo)) != NULL) { + /* Consume signals in FIFO order until the queue is empty. The queue may be + * consumed during the invocation of PHP signal handlers if new signals are + * delivered and PCNTL_G(pending_signals) is set. */ + while (pcntl_signal_dequeue(&sig)) { + if ((handle = zend_hash_index_find(&PCNTL_G(php_signal_table), sig.signo)) != NULL) { if (Z_TYPE_P(handle) != IS_LONG) { - ZVAL_LONG(¶ms[0], queue->signo); + ZVAL_LONG(¶ms[0], sig.signo); array_init(¶ms[1]); - pcntl_siginfo_to_zval(queue->signo, &queue->siginfo, ¶ms[1]); + pcntl_siginfo_to_zval(sig.signo, &sig.siginfo, ¶ms[1]); call_user_function(NULL, NULL, handle, &retval, 2, params); zval_ptr_dtor(¶ms[1]); @@ -1416,14 +1404,14 @@ zend_signal_interrupt_result pcntl_signal_dispatch(void) if (Z_TYPE(retval) == IS_OBJECT && Z_OBJCE(retval) == SignalReturn_ce) { if (zend_enum_fetch_case_id(Z_OBJ(retval)) == ZEND_ENUM_Pcntl_SignalReturn_Interrupt) { interrupt = true; - } else if (!zend_bitset_in(PCNTL_G(restart_syscalls), queue->signo)) { + } else if (!zend_bitset_in(PCNTL_G(restart_syscalls), sig.signo)) { interrupt = true; } } else if (Z_TYPE(retval) > IS_NULL) { zend_type_error("Signal handler must return a Pcntl\\SignalReturn or no value, %s returned", zend_zval_value_name(&retval)); interrupt = true; - } else if (!zend_bitset_in(PCNTL_G(restart_syscalls), queue->signo)) { + } else if (!zend_bitset_in(PCNTL_G(restart_syscalls), sig.signo)) { interrupt = true; } @@ -1435,32 +1423,18 @@ zend_signal_interrupt_result pcntl_signal_dispatch(void) } } } - - next = queue->next; - queue->next = PCNTL_G(spares); - PCNTL_G(spares) = queue; - queue = next; } /* drain the remaining in case of exception thrown */ - while (queue) { - next = queue->next; - queue->next = PCNTL_G(spares); - PCNTL_G(spares) = queue; - queue = next; + if (EG(exception)) { + while (pcntl_signal_dequeue(&sig)) { + /* pass */ + } } - PCNTL_G(pending_signals) = false; - - /* Re-enable queue */ - PCNTL_G(processing_signal_queue) = false; - /* Re-enable fiber switching */ zend_fiber_switch_unblock(); - /* return signal mask to previous state */ - sigprocmask(SIG_SETMASK, &old_mask, NULL); - return interrupt ? ZEND_SIGNAL_INTERRUPT : ZEND_SIGNAL_RESTART; } diff --git a/ext/pcntl/php_pcntl.h b/ext/pcntl/php_pcntl.h index 64d2e0ac6824..920be4fd178f 100644 --- a/ext/pcntl/php_pcntl.h +++ b/ext/pcntl/php_pcntl.h @@ -16,6 +16,7 @@ #define PHP_PCNTL_H #include "Zend/zend_bitset.h" +#include "Zend/zend_atomic.h" #include "pcntl_decl.h" #if defined(HAVE_DECL_WCONTINUED) && HAVE_DECL_WCONTINUED == 1 && defined(HAVE_WIFCONTINUED) @@ -42,13 +43,14 @@ struct php_pcntl_pending_signal { ZEND_BEGIN_MODULE_GLOBALS(pcntl) HashTable php_signal_table; - bool processing_signal_queue; volatile bool pending_signals; bool async_signals; /* some OSes define NSIG to be > UINT8_MAX */ uint16_t num_signals; int last_error; - struct php_pcntl_pending_signal *head, *tail, *spares; + struct php_pcntl_pending_signal *pending_signals_queue; /* ring buffer */ + zend_atomic_int pending_signals_head; /* consumer position */ + zend_atomic_int pending_signals_tail; /* producer position */ zend_bitset restart_syscalls; ZEND_END_MODULE_GLOBALS(pcntl) diff --git a/ext/pcntl/tests/signal_nested_delivery.phpt b/ext/pcntl/tests/signal_nested_delivery.phpt new file mode 100644 index 000000000000..6afc07dbd3ca --- /dev/null +++ b/ext/pcntl/tests/signal_nested_delivery.phpt @@ -0,0 +1,38 @@ +--TEST-- +Signals can be delivered during handler execution +--EXTENSIONS-- +pcntl +posix +--FILE-- + +--EXPECT-- +Handling SIGUSR1 + Handling SIGUSR1 + Handling SIGUSR2 + Done handling SIGUSR2 + Handling SIGUSR2 + Done handling SIGUSR2 + Done handling SIGUSR1 +Done handling SIGUSR1 diff --git a/ext/pcntl/tests/signal_nested_delivery_io.phpt b/ext/pcntl/tests/signal_nested_delivery_io.phpt new file mode 100644 index 000000000000..6e5da0c4f0df --- /dev/null +++ b/ext/pcntl/tests/signal_nested_delivery_io.phpt @@ -0,0 +1,47 @@ +--TEST-- +Signals can be delivered during handler execution: I/O +--EXTENSIONS-- +pcntl +posix +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Handling SIGUSR1 + Handling SIGUSR2 + Done handling SIGUSR2 +bool(false) +Done handling SIGUSR1