From eaf8fb0b8fce52dc269ae42aa100fcfc5735407e Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 30 Jun 2026 19:59:14 +0200 Subject: [PATCH 1/8] 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 b1ad3f4fe7f3..3c7f1a99cbb3 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 a0f094324426..2e735942cd51 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 a895afd3dbcd..a7b193fd1f4a 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 83ba5e7490cf..3db2647c724a 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 265b47e52dc7..9e2b3fad2ba5 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; } @@ -847,13 +858,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); @@ -1348,15 +1367,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 */ @@ -1364,9 +1384,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 */ @@ -1379,7 +1400,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) { @@ -1387,12 +1407,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; } } @@ -1422,11 +1457,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. */ @@ -1905,3 +1942,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 836abaeac7dd..30d0129723b5 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: @@ -901,7 +875,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 ) @@ -1079,7 +1053,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); @@ -1131,7 +1105,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 c966b7af0053..fa0ffa0d4d1e 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); @@ -288,7 +313,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 ); @@ -298,7 +323,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, @@ -307,7 +333,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 a9e2fa2b1d05..ca33bc2d0d69 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, @@ -850,7 +911,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, @@ -951,7 +1012,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 2a189c8e369591245f78547f9a4a9f62f80ea059 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 6 Jul 2026 19:17:07 +0200 Subject: [PATCH 2/8] 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 9301536458a6..a4f2d8cfd802 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 8811d02348b4797bfc4169becdee0221332a0f89 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 6 Jul 2026 13:50:46 +0200 Subject: [PATCH 3/8] 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 8a8fa4b3a567..00d8e6e66902 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 { @@ -653,6 +658,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 a09a2180921d..e166d3594bbc 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -258,6 +258,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* @@ -436,6 +442,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; @@ -608,6 +618,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. @@ -710,6 +724,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; @@ -759,6 +777,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 */ @@ -783,6 +805,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; @@ -828,6 +854,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) { @@ -974,6 +1004,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; } @@ -1188,6 +1222,10 @@ static int php_stream_flush_ex(php_stream *stream, bool 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 ); } @@ -1209,6 +1247,10 @@ PHPAPI ssize_t php_stream_write(php_stream *stream, const char *buf, size_t coun { ssize_t bytes; + if (php_stream_check_in_use(stream) != SUCCESS) { + return (ssize_t) -1; + } + if (count == 0) { return 0; } @@ -1316,6 +1358,10 @@ static bool php_stream_has_notifier(php_stream *stream) 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) { @@ -1425,6 +1471,10 @@ PHPAPI int php_stream_set_option(php_stream *stream, int option, int value, void { 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); } @@ -1475,6 +1525,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; @@ -1516,6 +1570,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(); } @@ -1604,7 +1662,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; } @@ -2234,6 +2291,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); @@ -2422,3 +2483,8 @@ PHPAPI int php_stream_scandir(const char *dirname, zend_string **namelist[], php efree(vector); 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 ca33bc2d0d69..977958ded0ab 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; } @@ -1103,6 +1129,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 4455a742d47f767311840d4ad6c94cbd7e9fc889 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 6 Jul 2026 16:15:08 +0200 Subject: [PATCH 4/8] 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 9e2b3fad2ba5..c7bedd1d16d7 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)); @@ -817,16 +810,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 @@ -1341,71 +1328,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]); @@ -1413,14 +1401,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; } @@ -1432,32 +1420,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 From f18f3f211b1d4aafb749c843ce8b53356e1e7abc Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 21 Jul 2026 16:58:19 +0200 Subject: [PATCH 5/8] Fix unit tests test_php_network_connect_socket_eintr_t[1-3] were testing different variation of struct timeval arithmetic that do not exist anymore. Kept only one of them, added a non-restarting variant. --- main/network.c | 2 + tests/unit/Makefile | 2 +- tests/unit/main/test_network.c | 167 +++++++++++++++------------------ 3 files changed, 79 insertions(+), 92 deletions(-) diff --git a/main/network.c b/main/network.c index 30d0129723b5..2c5c4c310965 100644 --- a/main/network.c +++ b/main/network.c @@ -385,6 +385,7 @@ PHPAPI int php_network_connect_socket(php_stream *stream, php_deadline_init(&deadline, timeout); n = php_pollfd_deadline(stream, sockfd, events, &deadline); if (n < 0) { + error = php_socket_errno(); ret = -1; } else if (n == 0) { error = PHP_TIMEOUT_ERROR_VALUE; @@ -393,6 +394,7 @@ PHPAPI int php_network_connect_socket(php_stream *stream, /* 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) { + error = php_socket_errno(); ret = -1; } } diff --git a/tests/unit/Makefile b/tests/unit/Makefile index 778a03328238..c0f147de2d49 100644 --- a/tests/unit/Makefile +++ b/tests/unit/Makefile @@ -5,7 +5,7 @@ COMMON_LDFLAGS = ../../.libs/libphp.a -lcmocka -lpthread -lm -ldl -lresolv -luti # Update paths in .github/workflows/unit-tests.yml when adding new test to make it run in PR when such file changes TESTS = main/test_network main/test_io_copy main/test_network_SRC = main/test_network.c -main/test_network_LDFLAGS = $(COMMON_LDFLAGS) -Wl,--wrap=connect,--wrap=poll,--wrap=getsockopt,--wrap=gettimeofday +main/test_network_LDFLAGS = $(COMMON_LDFLAGS) -Wl,--wrap=connect,--wrap=poll,--wrap=getsockopt,--wrap=gettimeofday,--wrap=clock_gettime main/test_io_copy_SRC = main/test_io_copy.c main/test_io_copy_LDFLAGS = $(COMMON_LDFLAGS) -Wl,--wrap=copy_file_range,--wrap=sendfile,--wrap=splice,--wrap=read,--wrap=write,--wrap=poll,--wrap=setsockopt diff --git a/tests/unit/main/test_network.c b/tests/unit/main/test_network.c index 3e0e6e37ed98..285fba41bb10 100644 --- a/tests/unit/main/test_network.c +++ b/tests/unit/main/test_network.c @@ -48,6 +48,31 @@ int __wrap_gettimeofday(struct timeval *time_Info, struct timezone *timezone_Inf return mock_type(int); } +// Mocked clock_gettime +int __wrap_clock_gettime(clockid_t clockid, struct timespec *tp) +{ + function_called(); + struct timespec *now = mock_ptr_type(struct timespec *); + if (now) { + tp->tv_sec = now->tv_sec; + tp->tv_nsec = now->tv_nsec; + } + return mock_type(int); +} + +// Signal handler stub: request a syscall restart so the EINTR retry loop +// continues. +static zend_signal_interrupt_result restart_on_signal(void) +{ + return ZEND_SIGNAL_RESTART; +} + +// Signal handler stub: request interruption. +static zend_signal_interrupt_result interrupt_on_signal(void) +{ + return ZEND_SIGNAL_INTERRUPT; +} + // Test successful connection static void test_php_network_connect_socket_immediate_success(void **state) { struct timeval timeout = { .tv_sec = 2, .tv_usec = 500000 }; @@ -57,7 +82,7 @@ static void test_php_network_connect_socket_immediate_success(void **state) { expect_function_call(__wrap_connect); will_return(__wrap_connect, 0); - int result = php_network_connect_socket(sockfd, NULL, 0, 0, &timeout, NULL, &error_code); + int result = php_network_connect_socket(NULL, sockfd, NULL, 0, 0, &timeout, NULL, &error_code); assert_int_equal(result, 0); assert_int_equal(error_code, 0); @@ -66,6 +91,7 @@ static void test_php_network_connect_socket_immediate_success(void **state) { // Test successful connection in progress followed by poll static void test_php_network_connect_socket_progress_success(void **state) { struct timeval timeout_tv = { .tv_sec = 2, .tv_usec = 500000 }; + struct timespec now = { .tv_sec = 1000, .tv_nsec = 0 }; php_socket_t sockfd = 12; int error_code = 0; @@ -73,10 +99,15 @@ static void test_php_network_connect_socket_progress_success(void **state) { expect_function_call(__wrap_connect); will_return(__wrap_connect, EINPROGRESS); - // Mock time setting - ignored - expect_function_call(__wrap_gettimeofday); - will_return(__wrap_gettimeofday, NULL); - will_return(__wrap_gettimeofday, 0); + // Mock clock_gettime when initializing the deadline + expect_function_call(__wrap_clock_gettime); + will_return(__wrap_clock_gettime, &now); + will_return(__wrap_clock_gettime, 0); + + // Mock clock_gettime when computing the remaining timeout + expect_function_call(__wrap_clock_gettime); + will_return(__wrap_clock_gettime, &now); + will_return(__wrap_clock_gettime, 0); // Mock poll to return success expect_function_call(__wrap_poll); @@ -88,37 +119,44 @@ static void test_php_network_connect_socket_progress_success(void **state) { will_return(__wrap_getsockopt, 0); // optval saved result will_return(__wrap_getsockopt, 0); // actual return value - int result = php_network_connect_socket(sockfd, NULL, 0, 0, &timeout_tv, NULL, &error_code); + int result = php_network_connect_socket(NULL, sockfd, NULL, 0, 0, &timeout_tv, NULL, &error_code); assert_int_equal(result, 0); assert_int_equal(error_code, 0); } -static void test_php_network_connect_socket_eintr_t1(void **state) { +static void test_php_network_connect_socket_eintr_restart(void **state) { struct timeval timeout_tv = { .tv_sec = 2, .tv_usec = 500000 }; - struct timeval start_time = { .tv_sec = 1000, .tv_usec = 0 }; // Initial time - struct timeval retry_time = { .tv_sec = 1001, .tv_usec = 200000 }; // Time after EINTR + struct timespec start_time = { .tv_sec = 1000, .tv_nsec = 0 }; // Initial time + struct timespec retry_time = { .tv_sec = 1001, .tv_nsec = 200000000 }; // Time after EINTR php_socket_t sockfd = 12; int error_code = 0; + zend_signal_interrupt_function = restart_on_signal; + // Mock connect to set EINPROGRESS expect_function_call(__wrap_connect); will_return(__wrap_connect, EINPROGRESS); - // Mock gettimeofday for initial call - expect_function_call(__wrap_gettimeofday); - will_return(__wrap_gettimeofday, &start_time); - will_return(__wrap_gettimeofday, 0); + // Mock clock_gettime when initializing the deadline + expect_function_call(__wrap_clock_gettime); + will_return(__wrap_clock_gettime, &start_time); + will_return(__wrap_clock_gettime, 0); + + // Mock clock_gettime for the first remaining-timeout computation (same instant) + expect_function_call(__wrap_clock_gettime); + will_return(__wrap_clock_gettime, &start_time); + will_return(__wrap_clock_gettime, 0); // Mock poll to return EINTR first expect_function_call(__wrap_poll); expect_value(__wrap_poll, timeout, 2500); will_return(__wrap_poll, -EINTR); - // Mock gettimeofday after EINTR - expect_function_call(__wrap_gettimeofday); - will_return(__wrap_gettimeofday, &retry_time); - will_return(__wrap_gettimeofday, 0); + // Mock clock_gettime after EINTR (time has advanced) + expect_function_call(__wrap_clock_gettime); + will_return(__wrap_clock_gettime, &retry_time); + will_return(__wrap_clock_gettime, 0); // Mock poll to succeed on retry expect_function_call(__wrap_poll); @@ -130,97 +168,45 @@ static void test_php_network_connect_socket_eintr_t1(void **state) { will_return(__wrap_getsockopt, 0); will_return(__wrap_getsockopt, 0); - int result = php_network_connect_socket(sockfd, NULL, 0, 0, &timeout_tv, NULL, &error_code); + int result = php_network_connect_socket(NULL, sockfd, NULL, 0, 0, &timeout_tv, NULL, &error_code); // Ensure the function succeeds assert_int_equal(result, 0); assert_int_equal(error_code, 0); } -static void test_php_network_connect_socket_eintr_t2(void **state) { - struct timeval timeout_tv = { .tv_sec = 2, .tv_usec = 1500000 }; - struct timeval start_time = { .tv_sec = 1000, .tv_usec = 300000 }; // Initial time - struct timeval retry_time = { .tv_sec = 1001, .tv_usec = 200000 }; // Time after EINTR +static void test_php_network_connect_socket_eintr_interrupt(void **state) { + struct timeval timeout_tv = { .tv_sec = 3, .tv_usec = 500000 }; + struct timespec start_time = { .tv_sec = 1000, .tv_nsec = 300000000 }; // Initial time php_socket_t sockfd = 12; int error_code = 0; - // Mock connect to set EINPROGRESS - expect_function_call(__wrap_connect); - will_return(__wrap_connect, EINPROGRESS); - - // Mock gettimeofday for initial call - expect_function_call(__wrap_gettimeofday); - will_return(__wrap_gettimeofday, &start_time); - will_return(__wrap_gettimeofday, 0); - - // Mock poll to return EINTR first - expect_function_call(__wrap_poll); - expect_value(__wrap_poll, timeout, 3500); - will_return(__wrap_poll, -EINTR); - - // Mock gettimeofday after EINTR - expect_function_call(__wrap_gettimeofday); - will_return(__wrap_gettimeofday, &retry_time); - will_return(__wrap_gettimeofday, 0); - - // Mock poll to succeed on retry - expect_function_call(__wrap_poll); - expect_value(__wrap_poll, timeout, 2600); - will_return(__wrap_poll, 1); - - // Mock no socket error - expect_function_call(__wrap_getsockopt); - will_return(__wrap_getsockopt, 0); // optval saved result - will_return(__wrap_getsockopt, 0); // actual return value - - int result = php_network_connect_socket(sockfd, NULL, 0, 0, &timeout_tv, NULL, &error_code); - - // Ensure the function succeeds - assert_int_equal(result, 0); - assert_int_equal(error_code, 0); -} - -static void test_php_network_connect_socket_eintr_t3(void **state) { - struct timeval timeout_tv = { .tv_sec = 2, .tv_usec = 500000 }; - struct timeval start_time = { .tv_sec = 1002, .tv_usec = 300000 }; // Initial time - struct timeval retry_time = { .tv_sec = 1001, .tv_usec = 2200000 }; // Time after EINTR - php_socket_t sockfd = 12; - int error_code = 0; + zend_signal_interrupt_function = interrupt_on_signal; // Mock connect to set EINPROGRESS expect_function_call(__wrap_connect); will_return(__wrap_connect, EINPROGRESS); - // Mock gettimeofday for initial call - expect_function_call(__wrap_gettimeofday); - will_return(__wrap_gettimeofday, &start_time); - will_return(__wrap_gettimeofday, 0); + // Mock clock_gettime when initializing the deadline + expect_function_call(__wrap_clock_gettime); + will_return(__wrap_clock_gettime, &start_time); + will_return(__wrap_clock_gettime, 0); + + // Mock clock_gettime for the first remaining-timeout computation (same instant) + expect_function_call(__wrap_clock_gettime); + will_return(__wrap_clock_gettime, &start_time); + will_return(__wrap_clock_gettime, 0); // Mock poll to return EINTR first expect_function_call(__wrap_poll); - expect_value(__wrap_poll, timeout, 2500); + expect_value(__wrap_poll, timeout, 3500); will_return(__wrap_poll, -EINTR); - // Mock gettimeofday after EINTR - expect_function_call(__wrap_gettimeofday); - will_return(__wrap_gettimeofday, &retry_time); - will_return(__wrap_gettimeofday, 0); - - // Mock poll to succeed on retry - expect_function_call(__wrap_poll); - expect_value(__wrap_poll, timeout, 1600); - will_return(__wrap_poll, 1); - - // Mock no socket error - expect_function_call(__wrap_getsockopt); - will_return(__wrap_getsockopt, 0); // optval saved result - will_return(__wrap_getsockopt, 0); // actual return value - - int result = php_network_connect_socket(sockfd, NULL, 0, 0, &timeout_tv, NULL, &error_code); + int result = php_network_connect_socket(NULL, sockfd, NULL, 0, 0, &timeout_tv, NULL, &error_code); // Ensure the function succeeds - assert_int_equal(result, 0); - assert_int_equal(error_code, 0); + assert_int_equal(result, -1); + assert_int_equal(error_code, EINTR); } // Test connection error (ECONNREFUSED) @@ -233,7 +219,7 @@ static void test_php_network_connect_socket_connect_error(void **state) { expect_function_call(__wrap_connect); will_return(__wrap_connect, ECONNREFUSED); - int result = php_network_connect_socket(sockfd, NULL, 0, 0, &timeout, NULL, &error_code); + int result = php_network_connect_socket(NULL, sockfd, NULL, 0, 0, &timeout, NULL, &error_code); // Ensure the function returns an error assert_int_equal(result, -1); @@ -245,9 +231,8 @@ int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_php_network_connect_socket_immediate_success), cmocka_unit_test(test_php_network_connect_socket_progress_success), - cmocka_unit_test(test_php_network_connect_socket_eintr_t1), - cmocka_unit_test(test_php_network_connect_socket_eintr_t2), - cmocka_unit_test(test_php_network_connect_socket_eintr_t3), + cmocka_unit_test(test_php_network_connect_socket_eintr_restart), + cmocka_unit_test(test_php_network_connect_socket_eintr_interrupt), cmocka_unit_test(test_php_network_connect_socket_connect_error), }; return cmocka_run_group_tests(tests, NULL, NULL); From d319e2e786c2d5b912c388d1e0cf9017abe031b8 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 7 Aug 2026 12:07:49 +0200 Subject: [PATCH 6/8] Improve tests --- ext/pcntl/tests/signal_nested_delivery.phpt | 40 ++++++++++++++ .../signal_nested_delivery_free_slot.phpt | 53 +++++++++++++++++++ .../tests/signal_nested_delivery_io.phpt | 6 ++- .../tests/streams/concurrent_access.phpt | 32 ++++------- .../streams/concurrent_access_fatal.phpt | 51 +++++++++--------- 5 files changed, 130 insertions(+), 52 deletions(-) create mode 100644 ext/pcntl/tests/signal_nested_delivery_free_slot.phpt diff --git a/ext/pcntl/tests/signal_nested_delivery.phpt b/ext/pcntl/tests/signal_nested_delivery.phpt index 6afc07dbd3ca..60f81e3cb929 100644 --- a/ext/pcntl/tests/signal_nested_delivery.phpt +++ b/ext/pcntl/tests/signal_nested_delivery.phpt @@ -1,5 +1,8 @@ --TEST-- Signals can be delivered during handler execution +--DESCRIPTION-- +pcntl_signal_dispatch() dispatches pending signals if new ones were queued since +the last call (PCNTL_G(pending_signals)=true), including in nested calls. --EXTENSIONS-- pcntl posix @@ -12,6 +15,8 @@ pcntl_signal(SIGUSR1, function ($signo) use (&$indent) { echo "{$indent}Handling SIGUSR1\n"; $indent .= ' '; posix_kill(posix_getpid(), SIGUSR2); + echo "{$indent}PCNTL_G(pending_signals)=true\n"; + echo "{$indent}Dispatch\n"; pcntl_signal_dispatch(); $indent = substr($indent, 0, -1); echo "{$indent}Done handling SIGUSR1\n"; @@ -24,15 +29,50 @@ pcntl_signal(SIGUSR2, function () use (&$indent) { posix_kill(posix_getpid(), SIGUSR1); posix_kill(posix_getpid(), SIGUSR1); +echo "PCNTL_G(pending_signals)=true\n"; +echo "Dispatch\n"; +pcntl_signal_dispatch(); + +echo "====\n"; + +pcntl_signal(SIGUSR1, function ($signo) use (&$indent) { + echo "{$indent}Handling SIGUSR1\n"; + $indent .= ' '; + echo "{$indent}Dispatch\n"; + // No-op as no new signal was delivered + pcntl_signal_dispatch(); + $indent = substr($indent, 0, -1); + echo "{$indent}Done handling SIGUSR1\n"; +}); + +posix_kill(posix_getpid(), SIGUSR1); +posix_kill(posix_getpid(), SIGUSR1); +echo "PCNTL_G(pending_signals)=true\n"; +echo "Dispatch\n"; pcntl_signal_dispatch(); ?> --EXPECT-- +PCNTL_G(pending_signals)=true +Dispatch Handling SIGUSR1 + PCNTL_G(pending_signals)=true + Dispatch Handling SIGUSR1 + PCNTL_G(pending_signals)=true + Dispatch Handling SIGUSR2 Done handling SIGUSR2 Handling SIGUSR2 Done handling SIGUSR2 Done handling SIGUSR1 Done handling SIGUSR1 +==== +PCNTL_G(pending_signals)=true +Dispatch +Handling SIGUSR1 + Dispatch +Done handling SIGUSR1 +Handling SIGUSR1 + Dispatch +Done handling SIGUSR1 diff --git a/ext/pcntl/tests/signal_nested_delivery_free_slot.phpt b/ext/pcntl/tests/signal_nested_delivery_free_slot.phpt new file mode 100644 index 000000000000..b6fa49910452 --- /dev/null +++ b/ext/pcntl/tests/signal_nested_delivery_free_slot.phpt @@ -0,0 +1,53 @@ +--TEST-- +At least one signal can be queued during dispatch +--DESCRIPTION-- +pcntl_signal_dispatch() dequeues a signal before dispatching it, so at least one +signal is deliverable during dispatch. +--EXTENSIONS-- +pcntl +posix +--FILE-- + +--EXPECT-- +Handling SIGUSR1 + Dispatch + More SIGUSR1... + Handling SIGUSR2 + Done handling SIGUSR2 +Done handling SIGUSR1 diff --git a/ext/pcntl/tests/signal_nested_delivery_io.phpt b/ext/pcntl/tests/signal_nested_delivery_io.phpt index 6e5da0c4f0df..3dd18bbfeca7 100644 --- a/ext/pcntl/tests/signal_nested_delivery_io.phpt +++ b/ext/pcntl/tests/signal_nested_delivery_io.phpt @@ -23,7 +23,9 @@ pcntl_signal(SIGUSR1, function ($signo) use (&$indent, $read) { // Queue signal posix_kill(posix_getpid(), SIGUSR2); // Delivered here - var_dump(fread($read, 1)); + $result = fread($read, 1); + echo "{$indent}fread(): "; + var_dump($result); $indent = substr($indent, 0, -1); echo "{$indent}Done handling SIGUSR1\n"; @@ -43,5 +45,5 @@ pcntl_signal_dispatch(); Handling SIGUSR1 Handling SIGUSR2 Done handling SIGUSR2 -bool(false) + fread(): bool(false) Done handling SIGUSR1 diff --git a/ext/standard/tests/streams/concurrent_access.phpt b/ext/standard/tests/streams/concurrent_access.phpt index 6d955fc61230..b4b9490cde3c 100644 --- a/ext/standard/tests/streams/concurrent_access.phpt +++ b/ext/standard/tests/streams/concurrent_access.phpt @@ -7,16 +7,11 @@ posix --FILE-- 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); -} +[$read, $write] = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); +stream_set_blocking($read, true); +stream_set_timeout($read, 0, 1); -// Wait for child to start -fread($read, 1); +// Queue signal +posix_kill(posix_getpid(), SIGUSR1); +// Signal handled here $result = fread($read, 1024); var_dump($result); -pcntl_waitpid($pid, $status); fclose($read); fclose($write); ?> @@ -56,5 +43,4 @@ fclose($write); handler Error: Concurrent access to a stream Error: Concurrent access to a stream -string(6) "hello -" +bool(false) diff --git a/ext/standard/tests/streams/concurrent_access_fatal.phpt b/ext/standard/tests/streams/concurrent_access_fatal.phpt index 4070bacf14a8..9c18ba42e9c6 100644 --- a/ext/standard/tests/streams/concurrent_access_fatal.phpt +++ b/ext/standard/tests/streams/concurrent_access_fatal.phpt @@ -1,54 +1,51 @@ --TEST-- -Stream is closed while marked in_use after a fatal error +Stream can still be closed after a fatal error, despise being marked IN_USE --EXTENSIONS-- pcntl posix +zend_test --SKIPIF-- --FILE-- getMessage(), "\n"; - return; + echo $e::class, ": ", $e->getMessage(), "\n"; } - echo "unreachable (shutdown function)\n"; + echo "End of shutdown function\n"; }); -// Wait for child to start -fread($read, 1); +[$read, $write] = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); +stream_set_blocking($read, true); +stream_set_timeout($read, 0, 1); + +// Queue signal +posix_kill(posix_getpid(), SIGUSR1); +// Signal handled here $result = fread($read, 1024); -echo "unreachable"; +echo "unreachable\n"; + +/* We are testing that no leaks occur */ + ?> --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 +Fatal error: Bailout in %s on line %d +Accessing the stream from a shutdown function should fail: +Error: Concurrent access to a stream +End of shutdown function From b93d1369d151a78a411438c923023e4059124c3a Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 7 Aug 2026 12:31:05 +0200 Subject: [PATCH 7/8] Naming --- main/php_streams.h | 6 +++--- main/streams/streams.c | 34 ++++++++++++++++------------------ main/streams/xp_socket.c | 14 ++++++-------- 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/main/php_streams.h b/main/php_streams.h index 00d8e6e66902..85fcc4423ea8 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -672,13 +672,13 @@ static inline zend_signal_interrupt_result php_stream_check_signals(php_stream * return zend_signal_interrupt_function(); } -PHPAPI ZEND_COLD void php_stream_in_use_error(void); +PHPAPI ZEND_COLD void php_stream_concurrent_access_error(void); /* See PHP_STREAM_FLAG_IN_USE */ -static inline zend_result php_stream_check_in_use(php_stream *stream) +static inline zend_result php_stream_check_concurrent_access(php_stream *stream) { if (UNEXPECTED(stream->flags & PHP_STREAM_FLAG_IN_USE)) { - php_stream_in_use_error(); + php_stream_concurrent_access_error(); return FAILURE; } return SUCCESS; diff --git a/main/streams/streams.c b/main/streams/streams.c index e166d3594bbc..555b24755d98 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -258,9 +258,7 @@ 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(); + if (UNEXPECTED(!(EG(flags) & EG_FLAGS_IN_RESOURCE_SHUTDOWN) && php_stream_check_concurrent_access(stream) != SUCCESS)) { return 0; } @@ -442,7 +440,7 @@ 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) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return FAILURE; } @@ -618,7 +616,7 @@ 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) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return 0; } @@ -724,7 +722,7 @@ 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) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return 0; } @@ -777,7 +775,7 @@ 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) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return -1; } @@ -805,7 +803,7 @@ 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) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return 0; } @@ -854,7 +852,7 @@ 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) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return NULL; } @@ -1004,7 +1002,7 @@ 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) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return NULL; } @@ -1222,7 +1220,7 @@ static int php_stream_flush_ex(php_stream *stream, bool closing) { int ret = 0; - if (php_stream_check_in_use(stream) != SUCCESS) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return -1; } @@ -1247,7 +1245,7 @@ PHPAPI ssize_t php_stream_write(php_stream *stream, const char *buf, size_t coun { ssize_t bytes; - if (php_stream_check_in_use(stream) != SUCCESS) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return (ssize_t) -1; } @@ -1358,7 +1356,7 @@ static bool php_stream_has_notifier(php_stream *stream) PHPAPI int php_stream_seek(php_stream *stream, zend_off_t offset, int whence) { - if (php_stream_check_in_use(stream) != SUCCESS) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return -1; } @@ -1471,7 +1469,7 @@ PHPAPI int php_stream_set_option(php_stream *stream, int option, int value, void { int ret = PHP_STREAM_OPTION_RETURN_NOTIMPL; - if (php_stream_check_in_use(stream) != SUCCESS) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return PHP_STREAM_OPTION_RETURN_ERR; } @@ -1525,7 +1523,7 @@ 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) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return -1; } @@ -1570,7 +1568,7 @@ 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) { + if (php_stream_check_concurrent_access(src) != SUCCESS) { return NULL; } @@ -2291,7 +2289,7 @@ 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) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return NULL; } @@ -2484,7 +2482,7 @@ PHPAPI int php_stream_scandir(const char *dirname, zend_string **namelist[], php return -1; } -PHPAPI ZEND_COLD void php_stream_in_use_error(void) +PHPAPI ZEND_COLD void php_stream_concurrent_access_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 977958ded0ab..a36e65da7906 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -69,7 +69,7 @@ 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) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return -1; } @@ -165,7 +165,7 @@ 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) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return -1; } @@ -238,9 +238,7 @@ 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(); + if (UNEXPECTED(!(EG(flags) & EG_FLAGS_IN_RESOURCE_SHUTDOWN) && php_stream_check_concurrent_access(stream) != SUCCESS)) { return -1; } @@ -301,7 +299,7 @@ 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) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return -1; } @@ -587,7 +585,7 @@ 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) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return FAILURE; } @@ -1129,7 +1127,7 @@ 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) { + if (php_stream_check_concurrent_access(stream) != SUCCESS) { return -1; } From 6b401195fe1356ad6f9f601de8deb940a002a1e5 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 7 Aug 2026 12:57:20 +0200 Subject: [PATCH 8/8] Fix maybe-uninitialized --- main/streams/xp_socket.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index a36e65da7906..7c9f0ca6bb1e 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -175,9 +175,7 @@ static ssize_t php_sockop_read(php_stream *stream, char *buf, size_t count) /* 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); - } + php_deadline_init(&deadline, &sock->timeout); restart: if (php_stream_check_signals(stream) == ZEND_SIGNAL_INTERRUPT) {