From b57051435c9202c8c81c300935400d5150ffc24c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Mon, 3 Jun 2024 09:54:41 +0000 Subject: [PATCH 01/21] Updated the stream_select() function so that it supports bigger file descriptor numbers without recompiling PHP on non-Windows OS, by passing the select() system call dynamically generated fd_sets with a custom fd_bigset struct type --- ext/standard/streamsfuncs.c | 325 +++++++++++++++++++++++++----------- 1 file changed, 227 insertions(+), 98 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index c4165836ecbe..b712a253e4f3 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -616,6 +616,122 @@ PHP_FUNCTION(stream_get_wrappers) } /* }}} */ +/* {{{ stream_select_big related macros and struct */ +typedef struct { +#ifdef __USE_XOPEN + char *fds_bits; +#else + char *__fds_bits; +#endif + size_t size; +} fd_bigset; + +#ifdef __USE_XOPEN +#define FD_BIGSET_ZERO(set, num_fds) do { \ + (set)->size = (num_fds + 7) / 8; \ + (set)->fds_bits = (char *)ecalloc((set)->size, sizeof(char)); \ +} while (0) +#define FD_BIGSET_SET(fd, set) ((set)->fds_bits[(fd) / 8] |= (1 << ((fd) % 8))) +#define FD_BIGSET_ISSET(fd, set) ((set)->fds_bits[(fd) / 8] & (1 << ((fd) % 8))) +#define FD_BIGSET_CLR(fd, set) ((set)->fds_bits[(fd) / 8] &= ~(1 << ((fd) % 8))) +#define FD_BIGSET_FREE(set) do { \ + if ((set)->fds_bits) { \ + efree((set)->fds_bits); \ + (set)->fds_bits = NULL; \ + } \ + (set)->size = 0; \ +} while (0) +#else +#define FD_BIGSET_ZERO(set, num_fds) do { \ + (set)->size = (num_fds + 7) / 8; \ + (set)->__fds_bits = (char *)ecalloc((set)->size, sizeof(char)); \ +} while (0) +#define FD_BIGSET_SET(fd, set) ((set)->__fds_bits[(fd) / 8] |= (1 << ((fd) % 8))) +#define FD_BIGSET_ISSET(fd, set) ((set)->__fds_bits[(fd) / 8] & (1 << ((fd) % 8))) +#define FD_BIGSET_CLR(fd, set) ((set)->__fds_bits[(fd) / 8] &= ~(1 << ((fd) % 8))) +#define FD_BIGSET_FREE(set) do { \ + if ((set)->__fds_bits) { \ + efree((set)->__fds_bits); \ + (set)->__fds_bits = NULL; \ + } \ + (set)->size = 0; \ +} while (0) +#endif + +int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t *max_fd) { + zval *elem; + php_stream *stream; + int cnt = 0; + + if (Z_TYPE_P(stream_array) != IS_ARRAY) { + return 0; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(stream_array), elem) { + php_socket_t this_fd; + + ZVAL_DEREF(elem); + php_stream_from_zval_no_verify(stream, elem); + if (stream == NULL) { + continue; + } + + if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != -1) { + FD_BIGSET_SET(this_fd, set); + + if (this_fd > *max_fd) { + *max_fd = this_fd; + } + cnt++; + } + } ZEND_HASH_FOREACH_END(); + + return cnt ? 1 : 0; +} +static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds) { + zval *elem, *dest_elem; + HashTable *ht; + php_stream *stream; + int ret = 0; + zend_string *key; + zend_ulong num_ind; + + if (Z_TYPE_P(stream_array) != IS_ARRAY) { + return 0; + } + ht = zend_new_array(zend_hash_num_elements(Z_ARRVAL_P(stream_array))); + + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(stream_array), num_ind, key, elem) { + php_socket_t this_fd; + + ZVAL_DEREF(elem); + php_stream_from_zval_no_verify(stream, elem); + if (stream == NULL) { + continue; + } + + if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != SOCK_ERR) { + if (FD_BIGSET_ISSET(this_fd, fds)) { + if (!key) { + dest_elem = zend_hash_index_update(ht, num_ind, elem); + } else { + dest_elem = zend_hash_update(ht, key, elem); + } + + zval_add_ref(dest_elem); + ret++; + continue; + } + } + } ZEND_HASH_FOREACH_END(); + + zval_ptr_dtor(stream_array); + ZVAL_ARR(stream_array, ht); + + return ret; +} +/* }}} */ + /* {{{ stream_select related functions */ static int stream_array_to_fd_set(zval *stream_array, fd_set *fds, php_socket_t *max_fd) { @@ -756,115 +872,128 @@ static int stream_array_emulate_read_fd_set(zval *stream_array) /* }}} */ /* {{{ Runs the select() system call on the sets of streams with a timeout specified by tv_sec and tv_usec */ -PHP_FUNCTION(stream_select) -{ - zval *r_array, *w_array, *e_array; - struct timeval tv, *tv_p = NULL; - fd_set rfds, wfds, efds; - php_socket_t max_fd = 0; - int retval, sets = 0; - zend_long sec, usec = 0; - bool secnull; - bool usecnull = 1; - int set_count, max_set_count = 0; - - ZEND_PARSE_PARAMETERS_START(4, 5) - Z_PARAM_ARRAY_EX2(r_array, 1, 1, 0) - Z_PARAM_ARRAY_EX2(w_array, 1, 1, 0) - Z_PARAM_ARRAY_EX2(e_array, 1, 1, 0) - Z_PARAM_LONG_OR_NULL(sec, secnull) - Z_PARAM_OPTIONAL - Z_PARAM_LONG_OR_NULL(usec, usecnull) - ZEND_PARSE_PARAMETERS_END(); - - FD_ZERO(&rfds); - FD_ZERO(&wfds); - FD_ZERO(&efds); - - if (r_array != NULL) { - set_count = stream_array_to_fd_set(r_array, &rfds, &max_fd); - if (set_count > max_set_count) - max_set_count = set_count; - sets += set_count; - } +PHP_FUNCTION(stream_select) { + zval *r_array, *w_array, *e_array; + struct timeval tv, *tv_p = NULL; + fd_bigset rfds, wfds, efds; + php_socket_t max_fd = 0; + int retval, sets = 0; + zend_long sec, usec = 0; + bool secnull; + bool usecnull = 1; + int set_count, max_set_count = 0; + + ZEND_PARSE_PARAMETERS_START(4, 5) + Z_PARAM_ARRAY_EX2(r_array, 1, 1, 0) + Z_PARAM_ARRAY_EX2(w_array, 1, 1, 0) + Z_PARAM_ARRAY_EX2(e_array, 1, 1, 0) + Z_PARAM_LONG_OR_NULL(sec, secnull) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(usec, usecnull) + ZEND_PARSE_PARAMETERS_END(); + + FD_BIGSET_ZERO(&rfds, 15000); // Adjust the number of file descriptors as needed + FD_BIGSET_ZERO(&wfds, 15000); + FD_BIGSET_ZERO(&efds, 15000); + + if (r_array != NULL) { + set_count = stream_array_to_fd_bigset(r_array, &rfds, &max_fd); + if (set_count > max_set_count) + max_set_count = set_count; + sets += set_count; + } - if (w_array != NULL) { - set_count = stream_array_to_fd_set(w_array, &wfds, &max_fd); - if (set_count > max_set_count) - max_set_count = set_count; - sets += set_count; - } + if (w_array != NULL) { + set_count = stream_array_to_fd_bigset(w_array, &wfds, &max_fd); + if (set_count > max_set_count) + max_set_count = set_count; + sets += set_count; + } - if (e_array != NULL) { - set_count = stream_array_to_fd_set(e_array, &efds, &max_fd); - if (set_count > max_set_count) - max_set_count = set_count; - sets += set_count; - } + if (e_array != NULL) { + set_count = stream_array_to_fd_bigset(e_array, &efds, &max_fd); + if (set_count > max_set_count) + max_set_count = set_count; + sets += set_count; + } - if (!sets) { - zend_value_error("No stream arrays were passed"); - RETURN_THROWS(); - } + if (!sets) { + zend_value_error("No stream arrays were passed"); + FD_BIGSET_FREE(&rfds); + FD_BIGSET_FREE(&wfds); + FD_BIGSET_FREE(&efds); + RETURN_THROWS(); + } - if (!PHP_SAFE_MAX_FD(max_fd, max_set_count)) { - RETURN_FALSE; - } + if (secnull && !usecnull) { + if (usec != 0) { + zend_argument_value_error(5, "must be null when argument #4 ($seconds) is null"); + FD_BIGSET_FREE(&rfds); + FD_BIGSET_FREE(&wfds); + FD_BIGSET_FREE(&efds); + RETURN_THROWS(); + } + } - if (secnull && !usecnull) { - if (usec != 0) { - zend_argument_value_error(5, "must be null when argument #4 ($seconds) is null"); - RETURN_THROWS(); - } - } + if (!secnull) { + if (sec < 0) { + zend_argument_value_error(4, "must be greater than or equal to 0"); + FD_BIGSET_FREE(&rfds); + FD_BIGSET_FREE(&wfds); + FD_BIGSET_FREE(&efds); + RETURN_THROWS(); + } else if (usec < 0) { + zend_argument_value_error(5, "must be greater than or equal to 0"); + FD_BIGSET_FREE(&rfds); + FD_BIGSET_FREE(&wfds); + FD_BIGSET_FREE(&efds); + RETURN_THROWS(); + } + + tv.tv_sec = (long)(sec + (usec / 1000000)); + tv.tv_usec = (long)(usec % 1000000); + tv_p = &tv; + } - /* If seconds is not set to null, build the timeval, else we wait indefinitely */ - if (!secnull) { - if (sec < 0) { - zend_argument_value_error(4, "must be greater than or equal to 0"); - RETURN_THROWS(); - } else if (usec < 0) { - zend_argument_value_error(5, "must be greater than or equal to 0"); - RETURN_THROWS(); - } + // Support for buffered data + if (r_array != NULL) { + retval = stream_array_emulate_read_fd_set(r_array); + if (retval > 0) { + if (w_array != NULL) { + zval_ptr_dtor(w_array); + ZVAL_EMPTY_ARRAY(w_array); + } + if (e_array != NULL) { + zval_ptr_dtor(e_array); + ZVAL_EMPTY_ARRAY(e_array); + } + FD_BIGSET_FREE(&rfds); + FD_BIGSET_FREE(&wfds); + FD_BIGSET_FREE(&efds); + RETURN_LONG(retval); + } + } - /* Windows, Solaris and BSD do not like microsecond values which are >= 1 sec */ - tv.tv_sec = (long)(sec + (usec / 1000000)); - tv.tv_usec = (long)(usec % 1000000); - tv_p = &tv; - } - - /* slight hack to support buffered data; if there is data sitting in the - * read buffer of any of the streams in the read array, let's pretend - * that we selected, but return only the readable sockets */ - if (r_array != NULL) { - retval = stream_array_emulate_read_fd_set(r_array); - if (retval > 0) { - if (w_array != NULL) { - zval_ptr_dtor(w_array); - ZVAL_EMPTY_ARRAY(w_array); - } - if (e_array != NULL) { - zval_ptr_dtor(e_array); - ZVAL_EMPTY_ARRAY(e_array); - } - RETURN_LONG(retval); - } - } + retval = php_select(max_fd + 1, (fd_set *)rfds.fds_bits, (fd_set *)wfds.fds_bits, (fd_set *)efds.fds_bits, tv_p); - retval = php_select(max_fd+1, &rfds, &wfds, &efds, tv_p); + if (retval == -1) { + php_error_docref(NULL, E_WARNING, "Unable to select [%d]: %s (max_fd=%d)", + errno, strerror(errno), max_fd); + FD_BIGSET_FREE(&rfds); + FD_BIGSET_FREE(&wfds); + FD_BIGSET_FREE(&efds); + RETURN_FALSE; + } - if (retval == -1) { - php_error_docref(NULL, E_WARNING, "Unable to select [%d]: %s (max_fd=%d)", - errno, strerror(errno), max_fd); - RETURN_FALSE; - } + if (r_array != NULL) stream_array_from_fd_bigset(r_array, &rfds); + if (w_array != NULL) stream_array_from_fd_bigset(w_array, &wfds); + if (e_array != NULL) stream_array_from_fd_bigset(e_array, &efds); - if (r_array != NULL) stream_array_from_fd_set(r_array, &rfds); - if (w_array != NULL) stream_array_from_fd_set(w_array, &wfds); - if (e_array != NULL) stream_array_from_fd_set(e_array, &efds); + FD_BIGSET_FREE(&rfds); + FD_BIGSET_FREE(&wfds); + FD_BIGSET_FREE(&efds); - RETURN_LONG(retval); + RETURN_LONG(retval); } /* }}} */ From 166a0e4396a725984860866bf807ee04bb0c124c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Mon, 3 Jun 2024 10:06:50 +0000 Subject: [PATCH 02/21] Removed unused functions. Unsure if this is correct, since I don't know if those functions could be referenced by other extensions. --- ext/standard/streamsfuncs.c | 92 +------------------------------------ 1 file changed, 1 insertion(+), 91 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index b712a253e4f3..60e8e3dc9452 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -616,7 +616,7 @@ PHP_FUNCTION(stream_get_wrappers) } /* }}} */ -/* {{{ stream_select_big related macros and struct */ +/* {{{ fd_bigset related macros and typedefs to remove FD_SETSIZE limitation in stream_select */ typedef struct { #ifdef __USE_XOPEN char *fds_bits; @@ -730,96 +730,6 @@ static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds) { return ret; } -/* }}} */ - -/* {{{ stream_select related functions */ -static int stream_array_to_fd_set(zval *stream_array, fd_set *fds, php_socket_t *max_fd) -{ - zval *elem; - php_stream *stream; - int cnt = 0; - - if (Z_TYPE_P(stream_array) != IS_ARRAY) { - return 0; - } - - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(stream_array), elem) { - /* Temporary int fd is needed for the STREAM data type on windows, passing this_fd directly to php_stream_cast() - would eventually bring a wrong result on x64. php_stream_cast() casts to int internally, and this will leave - the higher bits of a SOCKET variable uninitialized on systems with little endian. */ - php_socket_t this_fd; - - ZVAL_DEREF(elem); - php_stream_from_zval_no_verify(stream, elem); - if (stream == NULL) { - continue; - } - /* get the fd. - * NB: Most other code will NOT use the PHP_STREAM_CAST_INTERNAL flag - * when casting. It is only used here so that the buffered data warning - * is not displayed. - * */ - if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != -1) { - - PHP_SAFE_FD_SET(this_fd, fds); - - if (this_fd > *max_fd) { - *max_fd = this_fd; - } - cnt++; - } - } ZEND_HASH_FOREACH_END(); - return cnt ? 1 : 0; -} - -static int stream_array_from_fd_set(zval *stream_array, fd_set *fds) -{ - zval *elem, *dest_elem; - HashTable *ht; - php_stream *stream; - int ret = 0; - zend_string *key; - zend_ulong num_ind; - - if (Z_TYPE_P(stream_array) != IS_ARRAY) { - return 0; - } - ht = zend_new_array(zend_hash_num_elements(Z_ARRVAL_P(stream_array))); - - ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(stream_array), num_ind, key, elem) { - php_socket_t this_fd; - - ZVAL_DEREF(elem); - php_stream_from_zval_no_verify(stream, elem); - if (stream == NULL) { - continue; - } - /* get the fd - * NB: Most other code will NOT use the PHP_STREAM_CAST_INTERNAL flag - * when casting. It is only used here so that the buffered data warning - * is not displayed. - */ - if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != SOCK_ERR) { - if (PHP_SAFE_FD_ISSET(this_fd, fds)) { - if (!key) { - dest_elem = zend_hash_index_update(ht, num_ind, elem); - } else { - dest_elem = zend_hash_update(ht, key, elem); - } - - zval_add_ref(dest_elem); - ret++; - continue; - } - } - } ZEND_HASH_FOREACH_END(); - - /* destroy old array and add new one */ - zval_ptr_dtor(stream_array); - ZVAL_ARR(stream_array, ht); - - return ret; -} static int stream_array_emulate_read_fd_set(zval *stream_array) { From f8c53e5c1e4652b0c7dac4040ba08f1b3fb0ad4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Mon, 3 Jun 2024 10:26:18 +0000 Subject: [PATCH 03/21] Fix bug which was found by CI on php-src when commiting using a different architecture than what I developed this on. --- ext/standard/streamsfuncs.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 60e8e3dc9452..da95e3e3912b 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -884,8 +884,12 @@ PHP_FUNCTION(stream_select) { } } - retval = php_select(max_fd + 1, (fd_set *)rfds.fds_bits, (fd_set *)wfds.fds_bits, (fd_set *)efds.fds_bits, tv_p); - + #ifdef __USE_XOPEN + retval = php_select(max_fd + 1, (fd_set *)rfds.fds_bits, (fd_set *)wfds.fds_bits, (fd_set *)efds.fds_bits, tv_p); + #else + retval = php_select(max_fd + 1, (fd_set *)rfds.__fds_bits, (fd_set *)wfds.__fds_bits, (fd_set *)efds.__fds_bits, tv_p); + #endif + if (retval == -1) { php_error_docref(NULL, E_WARNING, "Unable to select [%d]: %s (max_fd=%d)", errno, strerror(errno), max_fd); From 5631e3b837043fdacc0f600c84f2d046993bcd1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Mon, 3 Jun 2024 13:52:54 +0000 Subject: [PATCH 04/21] Fixed mistake from lazyness. The function now properly finds the largest file descriptor id and allocates a correct size. --- ext/standard/streamsfuncs.c | 108 ++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 36 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index da95e3e3912b..16ce31a0aede 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -731,8 +731,35 @@ static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds) { return ret; } -static int stream_array_emulate_read_fd_set(zval *stream_array) -{ +static php_socket_t stream_array_get_max_fd(zval *array) { + zval *elem; + php_stream *stream; + php_socket_t max_fd = 0; + php_socket_t this_fd; + + if (Z_TYPE_P(array) != IS_ARRAY) { + return 0; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(array), elem) { + ZVAL_DEREF(elem); + php_stream_from_zval_no_verify(stream, elem); + if (stream == NULL) { + continue; + } + + if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != -1) { + if (this_fd > max_fd) { + max_fd = this_fd; + } + } + } ZEND_HASH_FOREACH_END(); + + return max_fd; +} + + +static int stream_array_emulate_read_fd_set(zval *stream_array) { zval *elem, *dest_elem; HashTable *ht; php_stream *stream; @@ -786,8 +813,8 @@ PHP_FUNCTION(stream_select) { zval *r_array, *w_array, *e_array; struct timeval tv, *tv_p = NULL; fd_bigset rfds, wfds, efds; - php_socket_t max_fd = 0; - int retval, sets = 0; + php_socket_t max_fd = -1; + int retval = 0; zend_long sec, usec = 0; bool secnull; bool usecnull = 1; @@ -802,39 +829,46 @@ PHP_FUNCTION(stream_select) { Z_PARAM_LONG_OR_NULL(usec, usecnull) ZEND_PARSE_PARAMETERS_END(); - FD_BIGSET_ZERO(&rfds, 15000); // Adjust the number of file descriptors as needed - FD_BIGSET_ZERO(&wfds, 15000); - FD_BIGSET_ZERO(&efds, 15000); - + // Determine the largest file descriptor if (r_array != NULL) { - set_count = stream_array_to_fd_bigset(r_array, &rfds, &max_fd); - if (set_count > max_set_count) - max_set_count = set_count; - sets += set_count; + max_fd = stream_array_get_max_fd(r_array); } - if (w_array != NULL) { - set_count = stream_array_to_fd_bigset(w_array, &wfds, &max_fd); - if (set_count > max_set_count) - max_set_count = set_count; - sets += set_count; + php_socket_t tmp_max_fd = stream_array_get_max_fd(w_array); + if (tmp_max_fd > max_fd) { + max_fd = tmp_max_fd; + } } - if (e_array != NULL) { - set_count = stream_array_to_fd_bigset(e_array, &efds, &max_fd); - if (set_count > max_set_count) - max_set_count = set_count; - sets += set_count; + php_socket_t tmp_max_fd = stream_array_get_max_fd(e_array); + if (tmp_max_fd > max_fd) { + max_fd = tmp_max_fd; + } } - if (!sets) { - zend_value_error("No stream arrays were passed"); - FD_BIGSET_FREE(&rfds); - FD_BIGSET_FREE(&wfds); - FD_BIGSET_FREE(&efds); + if (max_fd == -1) { + zend_value_error("No stream arrays were passed or all arrays were empty"); RETURN_THROWS(); } + // Allocate fd_bigset based on max_fd + FD_BIGSET_ZERO(&rfds, max_fd + 1); + FD_BIGSET_ZERO(&wfds, max_fd + 1); + FD_BIGSET_ZERO(&efds, max_fd + 1); + + // Populate the fd_bigset structures + if (r_array != NULL) { + stream_array_to_fd_bigset(r_array, &rfds, &max_fd); + } + + if (w_array != NULL) { + stream_array_to_fd_bigset(w_array, &wfds, &max_fd); + } + + if (e_array != NULL) { + stream_array_to_fd_bigset(e_array, &efds, &max_fd); + } + if (secnull && !usecnull) { if (usec != 0) { zend_argument_value_error(5, "must be null when argument #4 ($seconds) is null"); @@ -865,7 +899,9 @@ PHP_FUNCTION(stream_select) { tv_p = &tv; } - // Support for buffered data + /* slight hack to support buffered data; if there is data sitting in the + * read buffer of any of the streams in the read array, let's pretend + * that we selected, but return only the readable sockets */ if (r_array != NULL) { retval = stream_array_emulate_read_fd_set(r_array); if (retval > 0) { @@ -884,15 +920,14 @@ PHP_FUNCTION(stream_select) { } } - #ifdef __USE_XOPEN - retval = php_select(max_fd + 1, (fd_set *)rfds.fds_bits, (fd_set *)wfds.fds_bits, (fd_set *)efds.fds_bits, tv_p); - #else - retval = php_select(max_fd + 1, (fd_set *)rfds.__fds_bits, (fd_set *)wfds.__fds_bits, (fd_set *)efds.__fds_bits, tv_p); - #endif - +#ifdef __USE_XOPEN + retval = php_select(max_fd + 1, (fd_set *)rfds.fds_bits, (fd_set *)wfds.fds_bits, (fd_set *)efds.fds_bits, tv_p); +#else + retval = php_select(max_fd + 1, (fd_set *)rfds.__fds_bits, (fd_set *)wfds.__fds_bits, (fd_set *)efds.__fds_bits, tv_p); +#endif + if (retval == -1) { - php_error_docref(NULL, E_WARNING, "Unable to select [%d]: %s (max_fd=%d)", - errno, strerror(errno), max_fd); + php_error_docref(NULL, E_WARNING, "Unable to select [%d]: %s (max_fd=%d)", errno, strerror(errno), max_fd); FD_BIGSET_FREE(&rfds); FD_BIGSET_FREE(&wfds); FD_BIGSET_FREE(&efds); @@ -909,6 +944,7 @@ PHP_FUNCTION(stream_select) { RETURN_LONG(retval); } + /* }}} */ /* {{{ stream_context related functions */ From 581a8d5f765c15379860e697b86f20e65c41c9cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Mon, 3 Jun 2024 14:03:07 +0000 Subject: [PATCH 05/21] Apparently the CI on github finds stuff my building process does not find. --- ext/standard/streamsfuncs.c | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 16ce31a0aede..a08d91fe8363 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -818,7 +818,6 @@ PHP_FUNCTION(stream_select) { zend_long sec, usec = 0; bool secnull; bool usecnull = 1; - int set_count, max_set_count = 0; ZEND_PARSE_PARAMETERS_START(4, 5) Z_PARAM_ARRAY_EX2(r_array, 1, 1, 0) From b93d7549514742a668b91f0a0aeadc1388779cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Tue, 4 Jun 2024 11:35:11 +0000 Subject: [PATCH 06/21] Problems --- ext/standard/streamsfuncs.c | 104 +++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 48 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index a08d91fe8363..c2d47c838ac2 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -668,7 +668,10 @@ int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t * } ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(stream_array), elem) { - php_socket_t this_fd; + /* Temporary int fd is needed for the STREAM data type on windows, passing this_fd directly to php_stream_cast() + would eventually bring a wrong result on x64. php_stream_cast() casts to int internally, and this will leave + the higher bits of a SOCKET variable uninitialized on systems with little endian. */ + php_socket_t this_fd; ZVAL_DEREF(elem); php_stream_from_zval_no_verify(stream, elem); @@ -676,7 +679,12 @@ int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t * continue; } - if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != -1) { + /* get the fd. + * NB: Most other code will NOT use the PHP_STREAM_CAST_INTERNAL flag + * when casting. It is only used here so that the buffered data warning + * is not displayed. + * */ + if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != -1) { FD_BIGSET_SET(this_fd, set); if (this_fd > *max_fd) { @@ -685,9 +693,9 @@ int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t * cnt++; } } ZEND_HASH_FOREACH_END(); - return cnt ? 1 : 0; } + static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds) { zval *elem, *dest_elem; HashTable *ht; @@ -710,7 +718,12 @@ static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds) { continue; } - if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != SOCK_ERR) { + /* get the fd + * NB: Most other code will NOT use the PHP_STREAM_CAST_INTERNAL flag + * when casting. It is only used here so that the buffered data warning + * is not displayed. + */ + if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != SOCK_ERR) { if (FD_BIGSET_ISSET(this_fd, fds)) { if (!key) { dest_elem = zend_hash_index_update(ht, num_ind, elem); @@ -725,6 +738,7 @@ static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds) { } } ZEND_HASH_FOREACH_END(); + /* destroy old array and add new one */ zval_ptr_dtor(stream_array); ZVAL_ARR(stream_array, ht); @@ -734,11 +748,11 @@ static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds) { static php_socket_t stream_array_get_max_fd(zval *array) { zval *elem; php_stream *stream; - php_socket_t max_fd = 0; + php_socket_t max_fd = -1; php_socket_t this_fd; if (Z_TYPE_P(array) != IS_ARRAY) { - return 0; + return -1; } ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(array), elem) { @@ -748,7 +762,7 @@ static php_socket_t stream_array_get_max_fd(zval *array) { continue; } - if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != -1) { + if (php_stream_can_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL) && SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != -1) { if (this_fd > max_fd) { max_fd = this_fd; } @@ -815,6 +829,7 @@ PHP_FUNCTION(stream_select) { fd_bigset rfds, wfds, efds; php_socket_t max_fd = -1; int retval = 0; + int sets = 0; zend_long sec, usec = 0; bool secnull; bool usecnull = 1; @@ -828,52 +843,35 @@ PHP_FUNCTION(stream_select) { Z_PARAM_LONG_OR_NULL(usec, usecnull) ZEND_PARSE_PARAMETERS_END(); - // Determine the largest file descriptor + /* Determine the largest file descriptor */ if (r_array != NULL) { max_fd = stream_array_get_max_fd(r_array); + sets = sets + 1; } + if (w_array != NULL) { php_socket_t tmp_max_fd = stream_array_get_max_fd(w_array); - if (tmp_max_fd > max_fd) { - max_fd = tmp_max_fd; - } + if (tmp_max_fd > max_fd) { + max_fd = tmp_max_fd; + } + sets = sets + 1; } if (e_array != NULL) { php_socket_t tmp_max_fd = stream_array_get_max_fd(e_array); - if (tmp_max_fd > max_fd) { - max_fd = tmp_max_fd; - } - } - - if (max_fd == -1) { - zend_value_error("No stream arrays were passed or all arrays were empty"); - RETURN_THROWS(); - } - - // Allocate fd_bigset based on max_fd - FD_BIGSET_ZERO(&rfds, max_fd + 1); - FD_BIGSET_ZERO(&wfds, max_fd + 1); - FD_BIGSET_ZERO(&efds, max_fd + 1); - - // Populate the fd_bigset structures - if (r_array != NULL) { - stream_array_to_fd_bigset(r_array, &rfds, &max_fd); - } - - if (w_array != NULL) { - stream_array_to_fd_bigset(w_array, &wfds, &max_fd); + if (tmp_max_fd > max_fd) { + max_fd = tmp_max_fd; + } + sets = sets + 1; } - if (e_array != NULL) { - stream_array_to_fd_bigset(e_array, &efds, &max_fd); - } + if (!sets) { + zend_value_error("No stream arrays were passed"); + RETURN_THROWS(); + } if (secnull && !usecnull) { if (usec != 0) { zend_argument_value_error(5, "must be null when argument #4 ($seconds) is null"); - FD_BIGSET_FREE(&rfds); - FD_BIGSET_FREE(&wfds); - FD_BIGSET_FREE(&efds); RETURN_THROWS(); } } @@ -881,18 +879,13 @@ PHP_FUNCTION(stream_select) { if (!secnull) { if (sec < 0) { zend_argument_value_error(4, "must be greater than or equal to 0"); - FD_BIGSET_FREE(&rfds); - FD_BIGSET_FREE(&wfds); - FD_BIGSET_FREE(&efds); RETURN_THROWS(); } else if (usec < 0) { zend_argument_value_error(5, "must be greater than or equal to 0"); - FD_BIGSET_FREE(&rfds); - FD_BIGSET_FREE(&wfds); - FD_BIGSET_FREE(&efds); RETURN_THROWS(); } + /* Windows, Solaris and BSD do not like microsecond values which are >= 1 sec */ tv.tv_sec = (long)(sec + (usec / 1000000)); tv.tv_usec = (long)(usec % 1000000); tv_p = &tv; @@ -912,13 +905,28 @@ PHP_FUNCTION(stream_select) { zval_ptr_dtor(e_array); ZVAL_EMPTY_ARRAY(e_array); } - FD_BIGSET_FREE(&rfds); - FD_BIGSET_FREE(&wfds); - FD_BIGSET_FREE(&efds); RETURN_LONG(retval); } } + /* Allocate fd_bigset based on max_fd */ + FD_BIGSET_ZERO(&rfds, max_fd + 1); + FD_BIGSET_ZERO(&wfds, max_fd + 1); + FD_BIGSET_ZERO(&efds, max_fd + 1); + + /* Populate the fd_bigset structures */ + if (r_array != NULL) { + stream_array_to_fd_bigset(r_array, &rfds, &max_fd); + } + + if (w_array != NULL) { + stream_array_to_fd_bigset(w_array, &wfds, &max_fd); + } + + if (e_array != NULL) { + stream_array_to_fd_bigset(e_array, &efds, &max_fd); + } + #ifdef __USE_XOPEN retval = php_select(max_fd + 1, (fd_set *)rfds.fds_bits, (fd_set *)wfds.fds_bits, (fd_set *)efds.fds_bits, tv_p); #else From ae8d2ecdfc1b3703f7743d8d4757d87d42242ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Tue, 4 Jun 2024 22:54:47 +0000 Subject: [PATCH 07/21] Updated code setting the max_fds based on the ulimit for the process. --- ext/standard/streamsfuncs.c | 171 +++++++++++++++--------------------- 1 file changed, 72 insertions(+), 99 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index c2d47c838ac2..86d953b12c48 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -658,7 +658,7 @@ typedef struct { } while (0) #endif -int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t *max_fd) { +int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t *max_fd, long max_fds) { zval *elem; php_stream *stream; int cnt = 0; @@ -685,18 +685,19 @@ int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t * * is not displayed. * */ if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != -1) { - FD_BIGSET_SET(this_fd, set); - - if (this_fd > *max_fd) { - *max_fd = this_fd; + if (this_fd < max_fds) { // Check if the descriptor is within the limit + FD_BIGSET_SET(this_fd, set); + if (this_fd > *max_fd) { + *max_fd = this_fd; + } + cnt++; } - cnt++; } } ZEND_HASH_FOREACH_END(); + return cnt ? 1 : 0; } - -static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds) { +static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds, long max_fds) { zval *elem, *dest_elem; HashTable *ht; php_stream *stream; @@ -718,13 +719,8 @@ static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds) { continue; } - /* get the fd - * NB: Most other code will NOT use the PHP_STREAM_CAST_INTERNAL flag - * when casting. It is only used here so that the buffered data warning - * is not displayed. - */ - if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != SOCK_ERR) { - if (FD_BIGSET_ISSET(this_fd, fds)) { + if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != SOCK_ERR) { + if (this_fd < max_fds && FD_BIGSET_ISSET(this_fd, fds)) { // Check if within bounds and if the bit is set if (!key) { dest_elem = zend_hash_index_update(ht, num_ind, elem); } else { @@ -733,47 +729,18 @@ static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds) { zval_add_ref(dest_elem); ret++; - continue; } } } ZEND_HASH_FOREACH_END(); - /* destroy old array and add new one */ zval_ptr_dtor(stream_array); ZVAL_ARR(stream_array, ht); return ret; } -static php_socket_t stream_array_get_max_fd(zval *array) { - zval *elem; - php_stream *stream; - php_socket_t max_fd = -1; - php_socket_t this_fd; - - if (Z_TYPE_P(array) != IS_ARRAY) { - return -1; - } - - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(array), elem) { - ZVAL_DEREF(elem); - php_stream_from_zval_no_verify(stream, elem); - if (stream == NULL) { - continue; - } - - if (php_stream_can_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL) && SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != -1) { - if (this_fd > max_fd) { - max_fd = this_fd; - } - } - } ZEND_HASH_FOREACH_END(); - - return max_fd; -} - - -static int stream_array_emulate_read_fd_set(zval *stream_array) { +static int stream_array_emulate_read_fd_set(zval *stream_array) +{ zval *elem, *dest_elem; HashTable *ht; php_stream *stream; @@ -827,12 +794,12 @@ PHP_FUNCTION(stream_select) { zval *r_array, *w_array, *e_array; struct timeval tv, *tv_p = NULL; fd_bigset rfds, wfds, efds; - php_socket_t max_fd = -1; - int retval = 0; - int sets = 0; + php_socket_t max_fd = 0; + int retval, sets = 0; zend_long sec, usec = 0; bool secnull; bool usecnull = 1; + int set_count, max_set_count = 0; ZEND_PARSE_PARAMETERS_START(4, 5) Z_PARAM_ARRAY_EX2(r_array, 1, 1, 0) @@ -843,35 +810,53 @@ PHP_FUNCTION(stream_select) { Z_PARAM_LONG_OR_NULL(usec, usecnull) ZEND_PARSE_PARAMETERS_END(); - /* Determine the largest file descriptor */ +#ifdef PHP_WIN32 + /* Check if the Windows standard handle is redirected to file */ + long max_fds = FD_SETSIZE; +#else + /* Check if the file descriptor identifier is a terminal */ + long max_fds = sysconf(_SC_OPEN_MAX); +#endif + + FD_BIGSET_ZERO(&rfds, max_fds); + FD_BIGSET_ZERO(&wfds, max_fds); + FD_BIGSET_ZERO(&efds, max_fds); + if (r_array != NULL) { - max_fd = stream_array_get_max_fd(r_array); - sets = sets + 1; + set_count = stream_array_to_fd_bigset(r_array, &rfds, &max_fd, max_fds); + if (set_count > max_set_count) + max_set_count = set_count; + sets += set_count; } if (w_array != NULL) { - php_socket_t tmp_max_fd = stream_array_get_max_fd(w_array); - if (tmp_max_fd > max_fd) { - max_fd = tmp_max_fd; - } - sets = sets + 1; + set_count = stream_array_to_fd_bigset(w_array, &wfds, &max_fd, max_fds); + if (set_count > max_set_count) + max_set_count = set_count; + sets += set_count; } + if (e_array != NULL) { - php_socket_t tmp_max_fd = stream_array_get_max_fd(e_array); - if (tmp_max_fd > max_fd) { - max_fd = tmp_max_fd; - } - sets = sets + 1; + set_count = stream_array_to_fd_bigset(e_array, &efds, &max_fd, max_fds); + if (set_count > max_set_count) + max_set_count = set_count; + sets += set_count; } - if (!sets) { - zend_value_error("No stream arrays were passed"); - RETURN_THROWS(); - } + if (!sets) { + zend_value_error("No stream arrays were passed"); + FD_BIGSET_FREE(&rfds); + FD_BIGSET_FREE(&wfds); + FD_BIGSET_FREE(&efds); + RETURN_THROWS(); + } if (secnull && !usecnull) { if (usec != 0) { zend_argument_value_error(5, "must be null when argument #4 ($seconds) is null"); + FD_BIGSET_FREE(&rfds); + FD_BIGSET_FREE(&wfds); + FD_BIGSET_FREE(&efds); RETURN_THROWS(); } } @@ -879,21 +864,24 @@ PHP_FUNCTION(stream_select) { if (!secnull) { if (sec < 0) { zend_argument_value_error(4, "must be greater than or equal to 0"); + FD_BIGSET_FREE(&rfds); + FD_BIGSET_FREE(&wfds); + FD_BIGSET_FREE(&efds); RETURN_THROWS(); } else if (usec < 0) { zend_argument_value_error(5, "must be greater than or equal to 0"); + FD_BIGSET_FREE(&rfds); + FD_BIGSET_FREE(&wfds); + FD_BIGSET_FREE(&efds); RETURN_THROWS(); } - /* Windows, Solaris and BSD do not like microsecond values which are >= 1 sec */ tv.tv_sec = (long)(sec + (usec / 1000000)); tv.tv_usec = (long)(usec % 1000000); tv_p = &tv; } - /* slight hack to support buffered data; if there is data sitting in the - * read buffer of any of the streams in the read array, let's pretend - * that we selected, but return only the readable sockets */ + // Support for buffered data if (r_array != NULL) { retval = stream_array_emulate_read_fd_set(r_array); if (retval > 0) { @@ -905,45 +893,31 @@ PHP_FUNCTION(stream_select) { zval_ptr_dtor(e_array); ZVAL_EMPTY_ARRAY(e_array); } + FD_BIGSET_FREE(&rfds); + FD_BIGSET_FREE(&wfds); + FD_BIGSET_FREE(&efds); RETURN_LONG(retval); } } - /* Allocate fd_bigset based on max_fd */ - FD_BIGSET_ZERO(&rfds, max_fd + 1); - FD_BIGSET_ZERO(&wfds, max_fd + 1); - FD_BIGSET_ZERO(&efds, max_fd + 1); - - /* Populate the fd_bigset structures */ - if (r_array != NULL) { - stream_array_to_fd_bigset(r_array, &rfds, &max_fd); - } - - if (w_array != NULL) { - stream_array_to_fd_bigset(w_array, &wfds, &max_fd); - } - - if (e_array != NULL) { - stream_array_to_fd_bigset(e_array, &efds, &max_fd); - } - -#ifdef __USE_XOPEN - retval = php_select(max_fd + 1, (fd_set *)rfds.fds_bits, (fd_set *)wfds.fds_bits, (fd_set *)efds.fds_bits, tv_p); -#else - retval = php_select(max_fd + 1, (fd_set *)rfds.__fds_bits, (fd_set *)wfds.__fds_bits, (fd_set *)efds.__fds_bits, tv_p); -#endif - + #ifdef __USE_XOPEN + retval = php_select(max_fd + 1, (fd_set *)rfds.fds_bits, (fd_set *)wfds.fds_bits, (fd_set *)efds.fds_bits, tv_p); + #else + retval = php_select(max_fd + 1, (fd_set *)rfds.__fds_bits, (fd_set *)wfds.__fds_bits, (fd_set *)efds.__fds_bits, tv_p); + #endif + if (retval == -1) { - php_error_docref(NULL, E_WARNING, "Unable to select [%d]: %s (max_fd=%d)", errno, strerror(errno), max_fd); + php_error_docref(NULL, E_WARNING, "Unable to select [%d]: %s (max_fd=%d)", + errno, strerror(errno), max_fd); FD_BIGSET_FREE(&rfds); FD_BIGSET_FREE(&wfds); FD_BIGSET_FREE(&efds); RETURN_FALSE; } - if (r_array != NULL) stream_array_from_fd_bigset(r_array, &rfds); - if (w_array != NULL) stream_array_from_fd_bigset(w_array, &wfds); - if (e_array != NULL) stream_array_from_fd_bigset(e_array, &efds); + if (r_array != NULL) stream_array_from_fd_bigset(r_array, &rfds, max_fds); + if (w_array != NULL) stream_array_from_fd_bigset(w_array, &wfds, max_fds); + if (e_array != NULL) stream_array_from_fd_bigset(e_array, &efds, max_fds); FD_BIGSET_FREE(&rfds); FD_BIGSET_FREE(&wfds); @@ -951,7 +925,6 @@ PHP_FUNCTION(stream_select) { RETURN_LONG(retval); } - /* }}} */ /* {{{ stream_context related functions */ From 5fed836daa8ddda03da1bc143d9f947315e93d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Wed, 5 Jun 2024 07:57:31 +0000 Subject: [PATCH 08/21] Fixed order of some input tests that caused testing to fail --- ext/standard/streamsfuncs.c | 59 +++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 86d953b12c48..d9aabb79b1b6 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -625,7 +625,6 @@ typedef struct { #endif size_t size; } fd_bigset; - #ifdef __USE_XOPEN #define FD_BIGSET_ZERO(set, num_fds) do { \ (set)->size = (num_fds + 7) / 8; \ @@ -678,7 +677,6 @@ int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t * if (stream == NULL) { continue; } - /* get the fd. * NB: Most other code will NOT use the PHP_STREAM_CAST_INTERNAL flag * when casting. It is only used here so that the buffered data warning @@ -690,8 +688,8 @@ int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t * if (this_fd > *max_fd) { *max_fd = this_fd; } - cnt++; } + cnt++; } } ZEND_HASH_FOREACH_END(); @@ -719,6 +717,11 @@ static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds, long continue; } + /* get the fd. + * NB: Most other code will NOT use the PHP_STREAM_CAST_INTERNAL flag + * when casting. It is only used here so that the buffered data warning + * is not displayed. + */ if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != SOCK_ERR) { if (this_fd < max_fds && FD_BIGSET_ISSET(this_fd, fds)) { // Check if within bounds and if the bit is set if (!key) { @@ -728,11 +731,13 @@ static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds, long } zval_add_ref(dest_elem); - ret++; + ret++; + continue; } } } ZEND_HASH_FOREACH_END(); + /* destroy old array and add new one */ zval_ptr_dtor(stream_array); ZVAL_ARR(stream_array, ht); @@ -799,7 +804,7 @@ PHP_FUNCTION(stream_select) { zend_long sec, usec = 0; bool secnull; bool usecnull = 1; - int set_count, max_set_count = 0; + int set_count = 0, max_set_count = 0; ZEND_PARSE_PARAMETERS_START(4, 5) Z_PARAM_ARRAY_EX2(r_array, 1, 1, 0) @@ -824,22 +829,22 @@ PHP_FUNCTION(stream_select) { if (r_array != NULL) { set_count = stream_array_to_fd_bigset(r_array, &rfds, &max_fd, max_fds); - if (set_count > max_set_count) - max_set_count = set_count; - sets += set_count; - } + if (set_count > max_set_count) + max_set_count = set_count; + sets += set_count; + } if (w_array != NULL) { set_count = stream_array_to_fd_bigset(w_array, &wfds, &max_fd, max_fds); - if (set_count > max_set_count) - max_set_count = set_count; - sets += set_count; - } + if (set_count > max_set_count) + max_set_count = set_count; + sets += set_count; + } if (e_array != NULL) { set_count = stream_array_to_fd_bigset(e_array, &efds, &max_fd, max_fds); - if (set_count > max_set_count) - max_set_count = set_count; + if (set_count > max_set_count) + max_set_count = set_count; sets += set_count; } @@ -851,14 +856,16 @@ PHP_FUNCTION(stream_select) { RETURN_THROWS(); } - if (secnull && !usecnull) { - if (usec != 0) { - zend_argument_value_error(5, "must be null when argument #4 ($seconds) is null"); - FD_BIGSET_FREE(&rfds); - FD_BIGSET_FREE(&wfds); - FD_BIGSET_FREE(&efds); - RETURN_THROWS(); - } + if (max_set_count == 0) { + RETURN_FALSE; + } + + if (secnull && !usecnull && usec != 0) { + zend_argument_value_error(5, "must be null when argument #4 ($seconds) is null"); + FD_BIGSET_FREE(&rfds); + FD_BIGSET_FREE(&wfds); + FD_BIGSET_FREE(&efds); + RETURN_THROWS(); } if (!secnull) { @@ -881,8 +888,10 @@ PHP_FUNCTION(stream_select) { tv_p = &tv; } - // Support for buffered data - if (r_array != NULL) { + /* slight hack to support buffered data; if there is data sitting in the + * read buffer of any of the streams in the read array, let's pretend + * that we selected, but return only the readable sockets */ + if (r_array != NULL) { retval = stream_array_emulate_read_fd_set(r_array); if (retval > 0) { if (w_array != NULL) { From 7c07b7746ab2f968fb7ecb17ddb89884f4576133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Fri, 7 Jun 2024 14:24:15 +0000 Subject: [PATCH 09/21] Refactored code and improved safey of macros according to comments on the PR from github. --- ext/standard/streamsfuncs.c | 53 ++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index d9aabb79b1b6..d8635db75fc2 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -630,9 +630,17 @@ typedef struct { (set)->size = (num_fds + 7) / 8; \ (set)->fds_bits = (char *)ecalloc((set)->size, sizeof(char)); \ } while (0) -#define FD_BIGSET_SET(fd, set) ((set)->fds_bits[(fd) / 8] |= (1 << ((fd) % 8))) -#define FD_BIGSET_ISSET(fd, set) ((set)->fds_bits[(fd) / 8] & (1 << ((fd) % 8))) -#define FD_BIGSET_CLR(fd, set) ((set)->fds_bits[(fd) / 8] &= ~(1 << ((fd) % 8))) +#define FD_BIGSET_SET(fd, set) do { \ + if ((fd) / 8 < (set)->size) { \ + (set)->fds_bits[(fd) / 8] |= (1 << ((fd) % 8)); \ + } \ +} while (0) +#define FD_BIGSET_ISSET(fd, set) (((fd) / 8 < (set)->size) ? ((set)->fds_bits[(fd) / 8] & (1 << ((fd) % 8))) : 0) +#define FD_BIGSET_CLR(fd, set) do { \ + if ((fd) / 8 < (set)->size) { \ + (set)->fds_bits[(fd) / 8] &= ~(1 << ((fd) % 8)); \ + } \ +} while (0) #define FD_BIGSET_FREE(set) do { \ if ((set)->fds_bits) { \ efree((set)->fds_bits); \ @@ -645,9 +653,17 @@ typedef struct { (set)->size = (num_fds + 7) / 8; \ (set)->__fds_bits = (char *)ecalloc((set)->size, sizeof(char)); \ } while (0) -#define FD_BIGSET_SET(fd, set) ((set)->__fds_bits[(fd) / 8] |= (1 << ((fd) % 8))) -#define FD_BIGSET_ISSET(fd, set) ((set)->__fds_bits[(fd) / 8] & (1 << ((fd) % 8))) -#define FD_BIGSET_CLR(fd, set) ((set)->__fds_bits[(fd) / 8] &= ~(1 << ((fd) % 8))) +#define FD_BIGSET_SET(fd, set) do { \ + if ((fd) / 8 < (set)->size) { \ + (set)->__fds_bits[(fd) / 8] |= (1 << ((fd) % 8)); \ + } \ +} while (0) +#define FD_BIGSET_ISSET(fd, set) (((fd) / 8 < (set)->size) ? ((set)->__fds_bits[(fd) / 8] & (1 << ((fd) % 8))) : 0) +#define FD_BIGSET_CLR(fd, set) do { \ + if ((fd) / 8 < (set)->size) { \ + (set)->__fds_bits[(fd) / 8] &= ~(1 << ((fd) % 8)); \ + } \ +} while (0) #define FD_BIGSET_FREE(set) do { \ if ((set)->__fds_bits) { \ efree((set)->__fds_bits); \ @@ -657,6 +673,7 @@ typedef struct { } while (0) #endif + int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t *max_fd, long max_fds) { zval *elem; php_stream *stream; @@ -683,12 +700,10 @@ int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t * * is not displayed. * */ if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != -1) { - if (this_fd < max_fds) { // Check if the descriptor is within the limit - FD_BIGSET_SET(this_fd, set); - if (this_fd > *max_fd) { - *max_fd = this_fd; - } - } + FD_BIGSET_SET(this_fd, set); + if (this_fd > *max_fd) { + *max_fd = this_fd; + } cnt++; } } ZEND_HASH_FOREACH_END(); @@ -723,7 +738,7 @@ static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds, long * is not displayed. */ if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != SOCK_ERR) { - if (this_fd < max_fds && FD_BIGSET_ISSET(this_fd, fds)) { // Check if within bounds and if the bit is set + if (FD_BIGSET_ISSET(this_fd, fds)) { // Check if within bounds and if the bit is set if (!key) { dest_elem = zend_hash_index_update(ht, num_ind, elem); } else { @@ -821,6 +836,9 @@ PHP_FUNCTION(stream_select) { #else /* Check if the file descriptor identifier is a terminal */ long max_fds = sysconf(_SC_OPEN_MAX); + if (max_fds == -1) { + max_fds = FD_SETSIZE; + } #endif FD_BIGSET_ZERO(&rfds, max_fds); @@ -829,22 +847,25 @@ PHP_FUNCTION(stream_select) { if (r_array != NULL) { set_count = stream_array_to_fd_bigset(r_array, &rfds, &max_fd, max_fds); - if (set_count > max_set_count) + if (set_count > max_set_count) { max_set_count = set_count; + } sets += set_count; } if (w_array != NULL) { set_count = stream_array_to_fd_bigset(w_array, &wfds, &max_fd, max_fds); - if (set_count > max_set_count) + if (set_count > max_set_count) { max_set_count = set_count; + } sets += set_count; } if (e_array != NULL) { set_count = stream_array_to_fd_bigset(e_array, &efds, &max_fd, max_fds); - if (set_count > max_set_count) + if (set_count > max_set_count) { max_set_count = set_count; + } sets += set_count; } From cad406e885cecb93460eb00ba3b998b3bf2d61fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Tue, 25 Jun 2024 12:24:41 +0000 Subject: [PATCH 10/21] fix: Added a cap to the number of file descriptors it can poll, in case an extremely high ulimit -n is configured. --- ext/standard/streamsfuncs.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 662b01fd6168..949e56580d77 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -841,6 +841,13 @@ PHP_FUNCTION(stream_select) { } #endif + /* Cap file descriptor limit at 131072 unless FD_SETSIZE is greater */ + if (FD_SETSIZE > 131072) { + max_fds = FD_SETSIZE; + } else if (max_fds > 131072) { + max_fds = 131072; + } + FD_BIGSET_ZERO(&rfds, max_fds); FD_BIGSET_ZERO(&wfds, max_fds); FD_BIGSET_ZERO(&efds, max_fds); @@ -850,7 +857,7 @@ PHP_FUNCTION(stream_select) { if (set_count > max_set_count) { max_set_count = set_count; } - sets += set_count; + sets += set_count; } if (w_array != NULL) { @@ -929,13 +936,13 @@ PHP_FUNCTION(stream_select) { RETURN_LONG(retval); } } - + #ifdef __USE_XOPEN retval = php_select(max_fd + 1, (fd_set *)rfds.fds_bits, (fd_set *)wfds.fds_bits, (fd_set *)efds.fds_bits, tv_p); #else retval = php_select(max_fd + 1, (fd_set *)rfds.__fds_bits, (fd_set *)wfds.__fds_bits, (fd_set *)efds.__fds_bits, tv_p); #endif - + if (retval == -1) { php_error_docref(NULL, E_WARNING, "Unable to select [%d]: %s (max_fd=%d)", errno, strerror(errno), max_fd); From 9c3c9b580e5f11a5a4aa32ee74723eb4fa69611e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Mon, 1 Jul 2024 10:51:09 +0000 Subject: [PATCH 11/21] fix: Updated stream_select implementation to dynamically resize the fd_bigset structure whenever needed --- ext/standard/streamsfuncs.c | 81 +++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 949e56580d77..f42a237e76a1 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -618,19 +618,19 @@ PHP_FUNCTION(stream_get_wrappers) /* {{{ fd_bigset related macros and typedefs to remove FD_SETSIZE limitation in stream_select */ typedef struct { -#ifdef __USE_XOPEN char *fds_bits; -#else - char *__fds_bits; -#endif size_t size; } fd_bigset; -#ifdef __USE_XOPEN +#define FD_BIGSET_ENSURE_CAPACITY(fd, set) \ + if (((fd) / 8 >= (set)->size)) { \ + fd_bigset_double_size(set); \ + } #define FD_BIGSET_ZERO(set, num_fds) do { \ (set)->size = (num_fds + 7) / 8; \ (set)->fds_bits = (char *)ecalloc((set)->size, sizeof(char)); \ } while (0) #define FD_BIGSET_SET(fd, set) do { \ + FD_BIGSET_ENSURE_CAPACITY(fd, set) \ if ((fd) / 8 < (set)->size) { \ (set)->fds_bits[(fd) / 8] |= (1 << ((fd) % 8)); \ } \ @@ -648,31 +648,30 @@ typedef struct { } \ (set)->size = 0; \ } while (0) -#else -#define FD_BIGSET_ZERO(set, num_fds) do { \ - (set)->size = (num_fds + 7) / 8; \ - (set)->__fds_bits = (char *)ecalloc((set)->size, sizeof(char)); \ -} while (0) -#define FD_BIGSET_SET(fd, set) do { \ - if ((fd) / 8 < (set)->size) { \ - (set)->__fds_bits[(fd) / 8] |= (1 << ((fd) % 8)); \ - } \ -} while (0) -#define FD_BIGSET_ISSET(fd, set) (((fd) / 8 < (set)->size) ? ((set)->__fds_bits[(fd) / 8] & (1 << ((fd) % 8))) : 0) -#define FD_BIGSET_CLR(fd, set) do { \ - if ((fd) / 8 < (set)->size) { \ - (set)->__fds_bits[(fd) / 8] &= ~(1 << ((fd) % 8)); \ - } \ -} while (0) -#define FD_BIGSET_FREE(set) do { \ - if ((set)->__fds_bits) { \ - efree((set)->__fds_bits); \ - (set)->__fds_bits = NULL; \ - } \ - (set)->size = 0; \ -} while (0) -#endif +/* {{{ Function to double the size of an fd_bigset */ +void fd_bigset_double_size(fd_bigset *set) { + if (!set || !set->fds_bits) { + return; + } + + size_t old_size = set->size; + size_t new_size = old_size * 2; + + /* Allocate new memory block twice the size of the original */ + char *new_bits = (char *)ecalloc(new_size, sizeof(char)); + + /* Copy old bits to new memory block */ + memcpy(new_bits, set->fds_bits, old_size); + + /* Free the old memory block */ + efree(set->fds_bits); + + /* Update the structure */ + set->fds_bits = new_bits; + set->size = new_size; +} +/* }}} */ int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t *max_fd, long max_fds) { zval *elem; @@ -710,6 +709,7 @@ int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t * return cnt ? 1 : 0; } + static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds, long max_fds) { zval *elem, *dest_elem; HashTable *ht; @@ -841,10 +841,8 @@ PHP_FUNCTION(stream_select) { } #endif - /* Cap file descriptor limit at 131072 unless FD_SETSIZE is greater */ - if (FD_SETSIZE > 131072) { - max_fds = FD_SETSIZE; - } else if (max_fds > 131072) { + /* Cap file descriptor limit at 131072; the fd_bigset will be dynamically resized if needed. */ + if (max_fds > 131072) { max_fds = 131072; } @@ -936,12 +934,17 @@ PHP_FUNCTION(stream_select) { RETURN_LONG(retval); } } - - #ifdef __USE_XOPEN - retval = php_select(max_fd + 1, (fd_set *)rfds.fds_bits, (fd_set *)wfds.fds_bits, (fd_set *)efds.fds_bits, tv_p); - #else - retval = php_select(max_fd + 1, (fd_set *)rfds.__fds_bits, (fd_set *)wfds.__fds_bits, (fd_set *)efds.__fds_bits, tv_p); - #endif + + + /* Ensure all bitmaps are the same size (one or more may have been dynamically resized) */ + size_t largest = rfds.size; + if (largest < wfds.size) largest = wfds.size; + if (largest < efds.size) largest = efds.size; + while (largest > rfds.size) fd_bigset_double_size(&rfds); + while (largest > wfds.size) fd_bigset_double_size(&wfds); + while (largest > efds.size) fd_bigset_double_size(&efds); + + retval = php_select(max_fd + 1, (fd_set *)rfds.fds_bits, (fd_set *)wfds.fds_bits, (fd_set *)efds.fds_bits, tv_p); if (retval == -1) { php_error_docref(NULL, E_WARNING, "Unable to select [%d]: %s (max_fd=%d)", From 84a8e2826d735d3d50be91aec6250913ba0ef5b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Mon, 1 Jul 2024 11:06:05 +0000 Subject: [PATCH 12/21] fix: Unused argument max_fds removed from a couple of functions. --- ext/standard/streamsfuncs.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index f42a237e76a1..e7196098fdd2 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -673,7 +673,7 @@ void fd_bigset_double_size(fd_bigset *set) { } /* }}} */ -int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t *max_fd, long max_fds) { +int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t *max_fd) { zval *elem; php_stream *stream; int cnt = 0; @@ -710,7 +710,7 @@ int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t * return cnt ? 1 : 0; } -static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds, long max_fds) { +static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds) { zval *elem, *dest_elem; HashTable *ht; php_stream *stream; @@ -851,7 +851,7 @@ PHP_FUNCTION(stream_select) { FD_BIGSET_ZERO(&efds, max_fds); if (r_array != NULL) { - set_count = stream_array_to_fd_bigset(r_array, &rfds, &max_fd, max_fds); + set_count = stream_array_to_fd_bigset(r_array, &rfds, &max_fd); if (set_count > max_set_count) { max_set_count = set_count; } @@ -859,7 +859,7 @@ PHP_FUNCTION(stream_select) { } if (w_array != NULL) { - set_count = stream_array_to_fd_bigset(w_array, &wfds, &max_fd, max_fds); + set_count = stream_array_to_fd_bigset(w_array, &wfds, &max_fd); if (set_count > max_set_count) { max_set_count = set_count; } @@ -867,7 +867,7 @@ PHP_FUNCTION(stream_select) { } if (e_array != NULL) { - set_count = stream_array_to_fd_bigset(e_array, &efds, &max_fd, max_fds); + set_count = stream_array_to_fd_bigset(e_array, &efds, &max_fd); if (set_count > max_set_count) { max_set_count = set_count; } @@ -955,9 +955,9 @@ PHP_FUNCTION(stream_select) { RETURN_FALSE; } - if (r_array != NULL) stream_array_from_fd_bigset(r_array, &rfds, max_fds); - if (w_array != NULL) stream_array_from_fd_bigset(w_array, &wfds, max_fds); - if (e_array != NULL) stream_array_from_fd_bigset(e_array, &efds, max_fds); + if (r_array != NULL) stream_array_from_fd_bigset(r_array, &rfds); + if (w_array != NULL) stream_array_from_fd_bigset(w_array, &wfds); + if (e_array != NULL) stream_array_from_fd_bigset(e_array, &efds); FD_BIGSET_FREE(&rfds); FD_BIGSET_FREE(&wfds); From be6d2956e7caeab44e5040831420cb380dfed566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Tue, 2 Jul 2024 20:54:09 +0000 Subject: [PATCH 13/21] fix: Improvments suggested by @arnaud-lb --- ext/standard/streamsfuncs.c | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index e7196098fdd2..2c7330b447ff 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -622,7 +622,7 @@ typedef struct { size_t size; } fd_bigset; #define FD_BIGSET_ENSURE_CAPACITY(fd, set) \ - if (((fd) / 8 >= (set)->size)) { \ + if (UNEXPECTED(((fd) / 8 >= (set)->size))) { \ fd_bigset_double_size(set); \ } #define FD_BIGSET_ZERO(set, num_fds) do { \ @@ -631,9 +631,7 @@ typedef struct { } while (0) #define FD_BIGSET_SET(fd, set) do { \ FD_BIGSET_ENSURE_CAPACITY(fd, set) \ - if ((fd) / 8 < (set)->size) { \ - (set)->fds_bits[(fd) / 8] |= (1 << ((fd) % 8)); \ - } \ + (set)->fds_bits[(fd) / 8] |= (1 << ((fd) % 8)); \ } while (0) #define FD_BIGSET_ISSET(fd, set) (((fd) / 8 < (set)->size) ? ((set)->fds_bits[(fd) / 8] & (1 << ((fd) % 8))) : 0) #define FD_BIGSET_CLR(fd, set) do { \ @@ -650,25 +648,19 @@ typedef struct { } while (0) /* {{{ Function to double the size of an fd_bigset */ -void fd_bigset_double_size(fd_bigset *set) { - if (!set || !set->fds_bits) { - return; - } +static void fd_bigset_double_size(fd_bigset *set) { + ZEND_ASSERT(set && set->fds_bits); size_t old_size = set->size; size_t new_size = old_size * 2; - /* Allocate new memory block twice the size of the original */ - char *new_bits = (char *)ecalloc(new_size, sizeof(char)); - - /* Copy old bits to new memory block */ - memcpy(new_bits, set->fds_bits, old_size); + /* Reallocate memory block with twice the size of the original */ + set->fds_bits = erealloc(set->fds_bits, new_size); - /* Free the old memory block */ - efree(set->fds_bits); + /* Zero out the extra capacity */ + memset(set->fds_bits + old_size, 0, new_size - old_size); /* Update the structure */ - set->fds_bits = new_bits; set->size = new_size; } /* }}} */ @@ -831,13 +823,13 @@ PHP_FUNCTION(stream_select) { ZEND_PARSE_PARAMETERS_END(); #ifdef PHP_WIN32 - /* Check if the Windows standard handle is redirected to file */ + /* For Windows use FD_SETSIZE */ long max_fds = FD_SETSIZE; #else - /* Check if the file descriptor identifier is a terminal */ + /* Get max possible descriptor count */ long max_fds = sysconf(_SC_OPEN_MAX); if (max_fds == -1) { - max_fds = FD_SETSIZE; + max_fds = 131072; } #endif From 30aff57e8868e5782bd8dc23d8acb5a944674697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Tue, 22 Sep 2026 10:43:39 +0000 Subject: [PATCH 14/21] fix: keep Windows on native fd_set, fix indentation to match .editorconfig This unblocks the review that stalled in July 2024: arnaud-lb pointed out that casting the POSIX fd_bigset (a growable bitset) to a native fd_set would corrupt memory on Windows, since WinSock's fd_set is an array of SOCKET handles capped at FD_SETSIZE rather than a bitset indexed by descriptor number. Windows now keeps the original, unmodified fd_set based implementation (guarded by #ifdef PHP_WIN32), so the unlimited-fd support only changes behavior on POSIX where select() already has no such limitation. Also reindents the fd_bigset-related code from spaces to tabs per .editorconfig, addressing ramsey's outstanding review comment. A follow-up could implement arnaud-lb's suggested growable Windows fd_set design (php-src#14452 review thread) to bring parity there. Co-Authored-By: Claude Sonnet 5 --- ext/standard/streamsfuncs.c | 517 +++++++++++++++++++++++------------- 1 file changed, 335 insertions(+), 182 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 2c7330b447ff..26da16d40089 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -616,18 +616,28 @@ PHP_FUNCTION(stream_get_wrappers) } /* }}} */ -/* {{{ fd_bigset related macros and typedefs to remove FD_SETSIZE limitation in stream_select */ +#ifndef PHP_WIN32 +/* {{{ fd_bigset related macros and typedefs to remove the FD_SETSIZE limitation from stream_select() on + * POSIX-like systems. select() itself has no such limit there; only the fixed-size fd_set imposes the + * traditional 1024 descriptor ceiling, so we build a bitset that grows on demand instead. + * + * Windows keeps using the native fd_set (see stream_array_to_fd_set() below): there, fd_set is an array of + * SOCKET handles capped at FD_SETSIZE rather than a bitset indexed by descriptor number, and growing it + * requires a different approach that is left for a follow-up. */ typedef struct { - char *fds_bits; - size_t size; + char *fds_bits; + size_t size; } fd_bigset; + +static void fd_bigset_double_size(fd_bigset *set); + #define FD_BIGSET_ENSURE_CAPACITY(fd, set) \ - if (UNEXPECTED(((fd) / 8 >= (set)->size))) { \ - fd_bigset_double_size(set); \ - } + if (UNEXPECTED(((fd) / 8 >= (set)->size))) { \ + fd_bigset_double_size(set); \ + } #define FD_BIGSET_ZERO(set, num_fds) do { \ - (set)->size = (num_fds + 7) / 8; \ - (set)->fds_bits = (char *)ecalloc((set)->size, sizeof(char)); \ + (set)->size = ((num_fds) + 7) / 8; \ + (set)->fds_bits = (char *) ecalloc((set)->size, sizeof(char)); \ } while (0) #define FD_BIGSET_SET(fd, set) do { \ FD_BIGSET_ENSURE_CAPACITY(fd, set) \ @@ -635,24 +645,27 @@ typedef struct { } while (0) #define FD_BIGSET_ISSET(fd, set) (((fd) / 8 < (set)->size) ? ((set)->fds_bits[(fd) / 8] & (1 << ((fd) % 8))) : 0) #define FD_BIGSET_CLR(fd, set) do { \ - if ((fd) / 8 < (set)->size) { \ - (set)->fds_bits[(fd) / 8] &= ~(1 << ((fd) % 8)); \ - } \ + if ((fd) / 8 < (set)->size) { \ + (set)->fds_bits[(fd) / 8] &= ~(1 << ((fd) % 8)); \ + } \ } while (0) #define FD_BIGSET_FREE(set) do { \ - if ((set)->fds_bits) { \ - efree((set)->fds_bits); \ - (set)->fds_bits = NULL; \ - } \ - (set)->size = 0; \ + if ((set)->fds_bits) { \ + efree((set)->fds_bits); \ + (set)->fds_bits = NULL; \ + } \ + (set)->size = 0; \ } while (0) -/* {{{ Function to double the size of an fd_bigset */ -static void fd_bigset_double_size(fd_bigset *set) { +/* {{{ Doubles the backing storage of an fd_bigset */ +static void fd_bigset_double_size(fd_bigset *set) +{ + size_t old_size, new_size; + ZEND_ASSERT(set && set->fds_bits); - size_t old_size = set->size; - size_t new_size = old_size * 2; + old_size = set->size; + new_size = old_size * 2; /* Reallocate memory block with twice the size of the original */ set->fds_bits = erealloc(set->fds_bits, new_size); @@ -660,31 +673,31 @@ static void fd_bigset_double_size(fd_bigset *set) { /* Zero out the extra capacity */ memset(set->fds_bits + old_size, 0, new_size - old_size); - /* Update the structure */ - set->size = new_size; + set->size = new_size; } /* }}} */ -int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t *max_fd) { - zval *elem; - php_stream *stream; - int cnt = 0; +static int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t *max_fd) +{ + zval *elem; + php_stream *stream; + int cnt = 0; - if (Z_TYPE_P(stream_array) != IS_ARRAY) { - return 0; - } + if (Z_TYPE_P(stream_array) != IS_ARRAY) { + return 0; + } - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(stream_array), elem) { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(stream_array), elem) { /* Temporary int fd is needed for the STREAM data type on windows, passing this_fd directly to php_stream_cast() would eventually bring a wrong result on x64. php_stream_cast() casts to int internally, and this will leave the higher bits of a SOCKET variable uninitialized on systems with little endian. */ php_socket_t this_fd; - ZVAL_DEREF(elem); - php_stream_from_zval_no_verify(stream, elem); - if (stream == NULL) { - continue; - } + ZVAL_DEREF(elem); + php_stream_from_zval_no_verify(stream, elem); + if (stream == NULL) { + continue; + } /* get the fd. * NB: Most other code will NOT use the PHP_STREAM_CAST_INTERNAL flag * when casting. It is only used here so that the buffered data warning @@ -695,61 +708,155 @@ int stream_array_to_fd_bigset(zval *stream_array, fd_bigset *set, php_socket_t * if (this_fd > *max_fd) { *max_fd = this_fd; } - cnt++; - } - } ZEND_HASH_FOREACH_END(); + cnt++; + } + } ZEND_HASH_FOREACH_END(); - return cnt ? 1 : 0; + return cnt ? 1 : 0; } -static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds) { - zval *elem, *dest_elem; - HashTable *ht; - php_stream *stream; - int ret = 0; - zend_string *key; - zend_ulong num_ind; +static int stream_array_from_fd_bigset(zval *stream_array, fd_bigset *fds) +{ + zval *elem, *dest_elem; + HashTable *ht; + php_stream *stream; + int ret = 0; + zend_string *key; + zend_ulong num_ind; - if (Z_TYPE_P(stream_array) != IS_ARRAY) { - return 0; - } - ht = zend_new_array(zend_hash_num_elements(Z_ARRVAL_P(stream_array))); + if (Z_TYPE_P(stream_array) != IS_ARRAY) { + return 0; + } + ht = zend_new_array(zend_hash_num_elements(Z_ARRVAL_P(stream_array))); - ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(stream_array), num_ind, key, elem) { - php_socket_t this_fd; + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(stream_array), num_ind, key, elem) { + php_socket_t this_fd; - ZVAL_DEREF(elem); - php_stream_from_zval_no_verify(stream, elem); - if (stream == NULL) { - continue; - } + ZVAL_DEREF(elem); + php_stream_from_zval_no_verify(stream, elem); + if (stream == NULL) { + continue; + } /* get the fd. * NB: Most other code will NOT use the PHP_STREAM_CAST_INTERNAL flag * when casting. It is only used here so that the buffered data warning * is not displayed. */ - if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != SOCK_ERR) { - if (FD_BIGSET_ISSET(this_fd, fds)) { // Check if within bounds and if the bit is set - if (!key) { - dest_elem = zend_hash_index_update(ht, num_ind, elem); - } else { - dest_elem = zend_hash_update(ht, key, elem); - } - - zval_add_ref(dest_elem); - ret++; + if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != SOCK_ERR) { + if (FD_BIGSET_ISSET(this_fd, fds)) { + if (!key) { + dest_elem = zend_hash_index_update(ht, num_ind, elem); + } else { + dest_elem = zend_hash_update(ht, key, elem); + } + + zval_add_ref(dest_elem); + ret++; continue; - } - } - } ZEND_HASH_FOREACH_END(); + } + } + } ZEND_HASH_FOREACH_END(); /* destroy old array and add new one */ - zval_ptr_dtor(stream_array); - ZVAL_ARR(stream_array, ht); + zval_ptr_dtor(stream_array); + ZVAL_ARR(stream_array, ht); - return ret; + return ret; } +/* }}} */ +#else /* PHP_WIN32 */ +/* {{{ Windows keeps the original native fd_set based implementation for now (capped at FD_SETSIZE); see the + * comment above the fd_bigset typedef for why. */ +static int stream_array_to_fd_set(zval *stream_array, fd_set *fds, php_socket_t *max_fd) +{ + zval *elem; + php_stream *stream; + int cnt = 0; + + if (Z_TYPE_P(stream_array) != IS_ARRAY) { + return 0; + } + + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(stream_array), elem) { + /* Temporary int fd is needed for the STREAM data type on windows, passing this_fd directly to php_stream_cast() + would eventually bring a wrong result on x64. php_stream_cast() casts to int internally, and this will leave + the higher bits of a SOCKET variable uninitialized on systems with little endian. */ + php_socket_t this_fd; + + ZVAL_DEREF(elem); + php_stream_from_zval_no_verify(stream, elem); + if (stream == NULL) { + continue; + } + /* get the fd. + * NB: Most other code will NOT use the PHP_STREAM_CAST_INTERNAL flag + * when casting. It is only used here so that the buffered data warning + * is not displayed. + * */ + if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != -1) { + + PHP_SAFE_FD_SET(this_fd, fds); + + if (this_fd > *max_fd) { + *max_fd = this_fd; + } + cnt++; + } + } ZEND_HASH_FOREACH_END(); + return cnt ? 1 : 0; +} + +static int stream_array_from_fd_set(zval *stream_array, fd_set *fds) +{ + zval *elem, *dest_elem; + HashTable *ht; + php_stream *stream; + int ret = 0; + zend_string *key; + zend_ulong num_ind; + + if (Z_TYPE_P(stream_array) != IS_ARRAY) { + return 0; + } + ht = zend_new_array(zend_hash_num_elements(Z_ARRVAL_P(stream_array))); + + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(stream_array), num_ind, key, elem) { + php_socket_t this_fd; + + ZVAL_DEREF(elem); + php_stream_from_zval_no_verify(stream, elem); + if (stream == NULL) { + continue; + } + /* get the fd + * NB: Most other code will NOT use the PHP_STREAM_CAST_INTERNAL flag + * when casting. It is only used here so that the buffered data warning + * is not displayed. + */ + if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != SOCK_ERR) { + if (PHP_SAFE_FD_ISSET(this_fd, fds)) { + if (!key) { + dest_elem = zend_hash_index_update(ht, num_ind, elem); + } else { + dest_elem = zend_hash_update(ht, key, elem); + } + + zval_add_ref(dest_elem); + ret++; + continue; + } + } + } ZEND_HASH_FOREACH_END(); + + /* destroy old array and add new one */ + zval_ptr_dtor(stream_array); + ZVAL_ARR(stream_array, ht); + + return ret; +} +/* }}} */ +#endif /* PHP_WIN32 */ static int stream_array_emulate_read_fd_set(zval *stream_array) { @@ -801,162 +908,208 @@ static int stream_array_emulate_read_fd_set(zval *stream_array) } /* }}} */ -/* {{{ Runs the select() system call on the sets of streams with a timeout specified by tv_sec and tv_usec */ -PHP_FUNCTION(stream_select) { - zval *r_array, *w_array, *e_array; - struct timeval tv, *tv_p = NULL; - fd_bigset rfds, wfds, efds; - php_socket_t max_fd = 0; - int retval, sets = 0; - zend_long sec, usec = 0; - bool secnull; - bool usecnull = 1; - int set_count = 0, max_set_count = 0; - - ZEND_PARSE_PARAMETERS_START(4, 5) - Z_PARAM_ARRAY_EX2(r_array, 1, 1, 0) - Z_PARAM_ARRAY_EX2(w_array, 1, 1, 0) - Z_PARAM_ARRAY_EX2(e_array, 1, 1, 0) - Z_PARAM_LONG_OR_NULL(sec, secnull) - Z_PARAM_OPTIONAL - Z_PARAM_LONG_OR_NULL(usec, usecnull) - ZEND_PARSE_PARAMETERS_END(); +#ifndef PHP_WIN32 +# define STREAM_SELECT_FREE_SETS() do { \ + FD_BIGSET_FREE(&rfds); \ + FD_BIGSET_FREE(&wfds); \ + FD_BIGSET_FREE(&efds); \ +} while (0) +#else +# define STREAM_SELECT_FREE_SETS() do {} while (0) +#endif -#ifdef PHP_WIN32 - /* For Windows use FD_SETSIZE */ - long max_fds = FD_SETSIZE; +/* {{{ Runs the select() system call on the sets of streams with a timeout specified by tv_sec and tv_usec */ +PHP_FUNCTION(stream_select) +{ + zval *r_array, *w_array, *e_array; + struct timeval tv, *tv_p = NULL; +#ifndef PHP_WIN32 + fd_bigset rfds, wfds, efds; #else - /* Get max possible descriptor count */ + fd_set rfds, wfds, efds; +#endif + php_socket_t max_fd = 0; + int retval, sets = 0; + zend_long sec, usec = 0; + bool secnull; + bool usecnull = 1; + int set_count = 0, max_set_count = 0; + + ZEND_PARSE_PARAMETERS_START(4, 5) + Z_PARAM_ARRAY_EX2(r_array, 1, 1, 0) + Z_PARAM_ARRAY_EX2(w_array, 1, 1, 0) + Z_PARAM_ARRAY_EX2(e_array, 1, 1, 0) + Z_PARAM_LONG_OR_NULL(sec, secnull) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(usec, usecnull) + ZEND_PARSE_PARAMETERS_END(); + +#ifndef PHP_WIN32 + /* Get max possible descriptor count for the initial allocation; the fd_bigset grows on + * demand regardless, so this is only sizing a starting guess, not a hard limit. */ long max_fds = sysconf(_SC_OPEN_MAX); - if (max_fds == -1) { + if (max_fds <= 0) { max_fds = 131072; } -#endif - /* Cap file descriptor limit at 131072; the fd_bigset will be dynamically resized if needed. */ + /* Cap the initial allocation at 131072; fd_bigset will be dynamically resized if needed. */ if (max_fds > 131072) { max_fds = 131072; } - FD_BIGSET_ZERO(&rfds, max_fds); - FD_BIGSET_ZERO(&wfds, max_fds); - FD_BIGSET_ZERO(&efds, max_fds); + FD_BIGSET_ZERO(&rfds, max_fds); + FD_BIGSET_ZERO(&wfds, max_fds); + FD_BIGSET_ZERO(&efds, max_fds); - if (r_array != NULL) { - set_count = stream_array_to_fd_bigset(r_array, &rfds, &max_fd); + if (r_array != NULL) { + set_count = stream_array_to_fd_bigset(r_array, &rfds, &max_fd); if (set_count > max_set_count) { max_set_count = set_count; } sets += set_count; } - if (w_array != NULL) { - set_count = stream_array_to_fd_bigset(w_array, &wfds, &max_fd); + if (w_array != NULL) { + set_count = stream_array_to_fd_bigset(w_array, &wfds, &max_fd); if (set_count > max_set_count) { max_set_count = set_count; } sets += set_count; } - if (e_array != NULL) { - set_count = stream_array_to_fd_bigset(e_array, &efds, &max_fd); + if (e_array != NULL) { + set_count = stream_array_to_fd_bigset(e_array, &efds, &max_fd); if (set_count > max_set_count) { max_set_count = set_count; } - sets += set_count; - } + sets += set_count; + } +#else + FD_ZERO(&rfds); + FD_ZERO(&wfds); + FD_ZERO(&efds); - if (!sets) { - zend_value_error("No stream arrays were passed"); - FD_BIGSET_FREE(&rfds); - FD_BIGSET_FREE(&wfds); - FD_BIGSET_FREE(&efds); - RETURN_THROWS(); - } + if (r_array != NULL) { + set_count = stream_array_to_fd_set(r_array, &rfds, &max_fd); + if (set_count > max_set_count) { + max_set_count = set_count; + } + sets += set_count; + } + + if (w_array != NULL) { + set_count = stream_array_to_fd_set(w_array, &wfds, &max_fd); + if (set_count > max_set_count) { + max_set_count = set_count; + } + sets += set_count; + } + + if (e_array != NULL) { + set_count = stream_array_to_fd_set(e_array, &efds, &max_fd); + if (set_count > max_set_count) { + max_set_count = set_count; + } + sets += set_count; + } +#endif + if (!sets) { + zend_value_error("No stream arrays were passed"); + STREAM_SELECT_FREE_SETS(); + RETURN_THROWS(); + } + +#ifndef PHP_WIN32 if (max_set_count == 0) { RETURN_FALSE; } +#else + if (!PHP_SAFE_MAX_FD(max_fd, max_set_count)) { + RETURN_FALSE; + } +#endif - if (secnull && !usecnull && usec != 0) { + if (secnull && !usecnull && usec != 0) { zend_argument_value_error(5, "must be null when argument #4 ($seconds) is null"); - FD_BIGSET_FREE(&rfds); - FD_BIGSET_FREE(&wfds); - FD_BIGSET_FREE(&efds); + STREAM_SELECT_FREE_SETS(); RETURN_THROWS(); - } + } - if (!secnull) { - if (sec < 0) { - zend_argument_value_error(4, "must be greater than or equal to 0"); - FD_BIGSET_FREE(&rfds); - FD_BIGSET_FREE(&wfds); - FD_BIGSET_FREE(&efds); - RETURN_THROWS(); - } else if (usec < 0) { - zend_argument_value_error(5, "must be greater than or equal to 0"); - FD_BIGSET_FREE(&rfds); - FD_BIGSET_FREE(&wfds); - FD_BIGSET_FREE(&efds); - RETURN_THROWS(); - } - - tv.tv_sec = (long)(sec + (usec / 1000000)); - tv.tv_usec = (long)(usec % 1000000); - tv_p = &tv; - } + if (!secnull) { + if (sec < 0) { + zend_argument_value_error(4, "must be greater than or equal to 0"); + STREAM_SELECT_FREE_SETS(); + RETURN_THROWS(); + } else if (usec < 0) { + zend_argument_value_error(5, "must be greater than or equal to 0"); + STREAM_SELECT_FREE_SETS(); + RETURN_THROWS(); + } + + /* Windows, Solaris and BSD do not like microsecond values which are >= 1 sec */ + tv.tv_sec = (long)(sec + (usec / 1000000)); + tv.tv_usec = (long)(usec % 1000000); + tv_p = &tv; + } /* slight hack to support buffered data; if there is data sitting in the * read buffer of any of the streams in the read array, let's pretend * that we selected, but return only the readable sockets */ if (r_array != NULL) { - retval = stream_array_emulate_read_fd_set(r_array); - if (retval > 0) { - if (w_array != NULL) { - zval_ptr_dtor(w_array); - ZVAL_EMPTY_ARRAY(w_array); - } - if (e_array != NULL) { - zval_ptr_dtor(e_array); - ZVAL_EMPTY_ARRAY(e_array); - } - FD_BIGSET_FREE(&rfds); - FD_BIGSET_FREE(&wfds); - FD_BIGSET_FREE(&efds); - RETURN_LONG(retval); - } - } - + retval = stream_array_emulate_read_fd_set(r_array); + if (retval > 0) { + if (w_array != NULL) { + zval_ptr_dtor(w_array); + ZVAL_EMPTY_ARRAY(w_array); + } + if (e_array != NULL) { + zval_ptr_dtor(e_array); + ZVAL_EMPTY_ARRAY(e_array); + } + STREAM_SELECT_FREE_SETS(); + RETURN_LONG(retval); + } + } - /* Ensure all bitmaps are the same size (one or more may have been dynamically resized) */ - size_t largest = rfds.size; - if (largest < wfds.size) largest = wfds.size; - if (largest < efds.size) largest = efds.size; - while (largest > rfds.size) fd_bigset_double_size(&rfds); - while (largest > wfds.size) fd_bigset_double_size(&wfds); - while (largest > efds.size) fd_bigset_double_size(&efds); +#ifndef PHP_WIN32 + /* Ensure all bitmaps are the same size (one or more may have been dynamically resized) */ + { + size_t largest = rfds.size; + if (largest < wfds.size) largest = wfds.size; + if (largest < efds.size) largest = efds.size; + while (rfds.size < largest) fd_bigset_double_size(&rfds); + while (wfds.size < largest) fd_bigset_double_size(&wfds); + while (efds.size < largest) fd_bigset_double_size(&efds); + } - retval = php_select(max_fd + 1, (fd_set *)rfds.fds_bits, (fd_set *)wfds.fds_bits, (fd_set *)efds.fds_bits, tv_p); + retval = php_select(max_fd + 1, (fd_set *) rfds.fds_bits, (fd_set *) wfds.fds_bits, (fd_set *) efds.fds_bits, tv_p); +#else + retval = php_select(max_fd + 1, &rfds, &wfds, &efds, tv_p); +#endif - if (retval == -1) { - php_error_docref(NULL, E_WARNING, "Unable to select [%d]: %s (max_fd=%d)", - errno, strerror(errno), max_fd); - FD_BIGSET_FREE(&rfds); - FD_BIGSET_FREE(&wfds); - FD_BIGSET_FREE(&efds); - RETURN_FALSE; - } + if (retval == -1) { + php_error_docref(NULL, E_WARNING, "Unable to select [%d]: %s (max_fd=%d)", + errno, strerror(errno), max_fd); + STREAM_SELECT_FREE_SETS(); + RETURN_FALSE; + } - if (r_array != NULL) stream_array_from_fd_bigset(r_array, &rfds); - if (w_array != NULL) stream_array_from_fd_bigset(w_array, &wfds); - if (e_array != NULL) stream_array_from_fd_bigset(e_array, &efds); +#ifndef PHP_WIN32 + if (r_array != NULL) stream_array_from_fd_bigset(r_array, &rfds); + if (w_array != NULL) stream_array_from_fd_bigset(w_array, &wfds); + if (e_array != NULL) stream_array_from_fd_bigset(e_array, &efds); +#else + if (r_array != NULL) stream_array_from_fd_set(r_array, &rfds); + if (w_array != NULL) stream_array_from_fd_set(w_array, &wfds); + if (e_array != NULL) stream_array_from_fd_set(e_array, &efds); +#endif - FD_BIGSET_FREE(&rfds); - FD_BIGSET_FREE(&wfds); - FD_BIGSET_FREE(&efds); + STREAM_SELECT_FREE_SETS(); - RETURN_LONG(retval); + RETURN_LONG(retval); } +#undef STREAM_SELECT_FREE_SETS /* }}} */ /* {{{ stream_context related functions */ From 9df943c31edd7453d83d035f7e2edf1de877602b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Tue, 22 Sep 2026 11:21:33 +0000 Subject: [PATCH 15/21] fix: two heap-safety bugs in fd_bigset found by code review 1. FD_BIGSET_ENSURE_CAPACITY used `if` instead of `while`, so a single fd jump larger than 2x the current capacity (reachable once the process's real fd limit exceeds the 131072 cap applied to the initial allocation) only doubled once, leaving the buffer too small for the fd actually being set -- an out-of-bounds heap write. Confirmed with a standalone ASan reproducer (heap-buffer-overflow). 2. FD_BIGSET_ZERO sized the buffer as ceil(num_fds/8) bytes, but select() operates on whole `long`-sized words at the syscall boundary (Linux's FDS_BYTES()/NFDBITS: it always touches ceil(nfds/(8*sizeof(long)))*sizeof(long) bytes for each fd_set argument). Whenever num_fds wasn't a multiple of 8*sizeof(long) (64 on 64-bit builds), our buffer was up to 7 bytes short, so select() read/wrote past the allocation into adjacent heap memory. Confirmed with a guard-page reproducer: the kernel returned EFAULT when the extra bytes landed on an unmapped page, proving it does touch memory beyond a too-small buffer; with a normal heap allocation those same bytes would instead silently corrupt whatever object happens to sit next to it. Both fixed by growing via a loop (not a single doubling) and by rounding the allocation up to a whole `long`, which doubling then preserves. Verified against the full ext/standard/tests/streams suite (no regressions) and manually with 300,000 real file descriptors (exceeding the threshold where a single doubling was insufficient). Co-Authored-By: Claude Sonnet 5 --- ext/standard/streamsfuncs.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 07d0adb9e82f..5c5cd4c6ebb1 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -663,11 +663,18 @@ typedef struct { static void fd_bigset_double_size(fd_bigset *set); #define FD_BIGSET_ENSURE_CAPACITY(fd, set) \ - if (UNEXPECTED(((fd) / 8 >= (set)->size))) { \ + while (UNEXPECTED(((fd) / 8 >= (set)->size))) { \ fd_bigset_double_size(set); \ } +/* select() operates on whole `long`-sized words (see e.g. Linux's FDS_BYTES()/NFDBITS): + * it always reads/writes size-rounded-up-to-sizeof(long) bytes for a given nfds, even + * though the traditional fd_set is only ever created at that exact alignment (FD_SETSIZE + * is a multiple of 8*sizeof(long)). Since our size is derived from an arbitrary fd count, + * we must round up to a whole `long` ourselves, or select() will read/write past our + * heap allocation into adjacent memory whenever num_fds isn't a multiple of 8*sizeof(long). */ #define FD_BIGSET_ZERO(set, num_fds) do { \ - (set)->size = ((num_fds) + 7) / 8; \ + size_t num_longs = ((size_t) (num_fds) + (8 * sizeof(long)) - 1) / (8 * sizeof(long)); \ + (set)->size = num_longs * sizeof(long); \ (set)->fds_bits = (char *) ecalloc((set)->size, sizeof(char)); \ } while (0) #define FD_BIGSET_SET(fd, set) do { \ From d2b1decb0476ffde41d2b41f52c8bfc0769a0d89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Tue, 22 Sep 2026 11:28:40 +0000 Subject: [PATCH 16/21] Update GH-9590 tests: fixing the fd_bigset limitation, not papering over it These tests (arnaud-lb, GH-9602/80232de0e4b) verified that stream_select() fails loudly and immediately once any watched descriptor exceeds FD_SETSIZE, instead of silently dropping it from the fd_set and blocking forever on a socket it can no longer see -- that was the real GH-9590 bug: a silently-dropped fd caused an indefinite, unexplained hang. Our fd_bigset removes the cause of that bug on POSIX rather than just detecting it: a descriptor past 1024 is no longer dropped, it's tracked correctly. So the warning path these tests expected is now dead code for this scenario, and running the tests unmodified exposed exactly the failure mode they were designed to prevent: with nothing ever written to the watched socket and no signal to stop it, `stream_select()` now genuinely blocks on the (correctly-registered) high fd for the full PHP_INT_MAX timeout instead of bailing out early, timing out CI after 2 minutes per test on FreeBSD. Updated both to assert the actual guarantee this PR provides: writing to the paired socket and confirming stream_select() correctly reports it as readable even past the old limit, with a finite timeout. Verified manually against the patched build (posix_setrlimit's hard-limit-raise via -1 fails as EPERM in this sandbox, so run-tests.php's own SKIPIF skips them here, but the exact --FILE-- logic was run standalone with an explicit hard limit and produces the new --EXPECT-- exactly). Co-Authored-By: Claude Sonnet 5 --- ext/standard/tests/streams/gh9590-001.phpt | 17 ++++++++--------- ext/standard/tests/streams/gh9590-002.phpt | 18 +++++++++--------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/ext/standard/tests/streams/gh9590-001.phpt b/ext/standard/tests/streams/gh9590-001.phpt index 8f5691af1ade..d14247a7cc59 100644 --- a/ext/standard/tests/streams/gh9590-001.phpt +++ b/ext/standard/tests/streams/gh9590-001.phpt @@ -1,5 +1,6 @@ --TEST-- -Bug GH-9590 001 (stream_select does not abort upon exception or empty valid fd set) +Bug GH-9590 001 (stream_select works correctly, without warning, past the traditional +FD_SETSIZE descriptor limit on platforms where that limit has been lifted) --EXTENSIONS-- posix --SKIPIF-- @@ -21,19 +22,17 @@ for ($i = 0; $i < 1023; $i++) { } list($a, $b) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); +fwrite($b, "x"); $r = [$a]; $w = $e = []; -var_dump(stream_select($r, $w, $e, PHP_INT_MAX)); +var_dump(stream_select($r, $w, $e, 30)); +var_dump(fread($a, 1)); ?> ---EXPECTF-- -Warning: stream_select(): You MUST recompile PHP with a larger value of FD_SETSIZE. -It is set to 1024, but you have descriptors numbered at least as high as %d. - --enable-fd-setsize=%d is recommended, but you may want to set it -to equal the maximum number of open files supported by your system, -in order to avoid seeing this error again at a later date. in %s on line %d -bool(false) +--EXPECT-- +int(1) +string(1) "x" --CLEAN-- ---EXPECTF-- -Fatal error: Uncaught Exception: stream_select(): You MUST recompile PHP with a larger value of FD_SETSIZE. -It is set to 1024, but you have descriptors numbered at least as high as %d. - --enable-fd-setsize=%d is recommended, but you may want to set it -to equal the maximum number of open files supported by your system, -in order to avoid seeing this error again at a later date. in %s:%d -Stack trace:%a +--EXPECT-- +int(1) +string(1) "x" --CLEAN-- Date: Tue, 22 Sep 2026 14:30:20 +0200 Subject: [PATCH 17/21] Windows: grow stream_select() fd_set on demand and cap handle count On Windows the winsock fd_set is a packed array of SOCKETs bounded by the compile-time FD_SETSIZE, so stream_select() silently dropped sockets past that limit. Replace the fixed fd_set on the socket path with a growable {capacity; fd_set} that doubles via realloc, giving parity with POSIX for socket-heavy code. FD_ISSET/FD_ZERO/select() only read fd_count, so the grown set is passed to them unchanged; php_select()'s internal working sets grow too and the struct-assignment copies become memcpy. The POSIX fd_bigset path and php_select()'s fd_set* ABI are unchanged. Also cap the non-socket (pipe/file) handle path at MAXIMUM_WAIT_OBJECTS (64) inside php_select() itself: it previously pushed each handle into a fixed 64-entry stack array with no bound, crashing the process with a stack buffer overrun (STATUS_STACK_BUFFER_OVERRUN) when given 65+ pipe or file handles. It now returns an error so the caller reports a warning and false. Fixing it in php_select() also protects the direct caller in sapi/cli/php_cli_server.c. Adds two Windows-only phpt tests: one selecting on more than FD_SETSIZE sockets, and one asserting 65+ handles fail gracefully instead of crashing. --- NEWS | 6 ++ UPGRADING | 7 ++ ext/standard/streamsfuncs.c | 26 +++--- .../stream_select_win32_many_sockets.phpt | 44 +++++++++ .../stream_select_win32_max_handles.phpt | 40 +++++++++ main/php_network.h | 69 ++++++++++++++ win32/select.c | 90 +++++++++++++------ 7 files changed, 243 insertions(+), 39 deletions(-) create mode 100644 ext/standard/tests/streams/stream_select_win32_many_sockets.phpt create mode 100644 ext/standard/tests/streams/stream_select_win32_max_handles.phpt diff --git a/NEWS b/NEWS index c91c7ce877af..02b6f045a029 100644 --- a/NEWS +++ b/NEWS @@ -95,6 +95,12 @@ PHP NEWS . Fixed three Windows-only proc_open() defects: an uninitialized PROCESS_INFORMATION, an indeterminate comspec pointer after a failed lookup, and an unchecked CreateFileA() failure. (Ilia Alshanetsky) + . Fixed stream_select() on Windows overflowing a fixed 64-entry stack array + (crashing with a stack buffer overrun) when passed more than 64 pipe or file + handles; it now fails gracefully instead. (Frode Børli) + . Removed the FD_SETSIZE ceiling on the number of sockets stream_select() can + watch on Windows; the winsock fd_set now grows on demand, matching POSIX. + (Frode Børli) - Zlib: . Fixed inflate_init() dropping the preset dictionary for raw streams with diff --git a/UPGRADING b/UPGRADING index 7f1fc588bd03..484b8f3410ee 100644 --- a/UPGRADING +++ b/UPGRADING @@ -1070,6 +1070,13 @@ PHP 8.6 UPGRADE NOTES . The OpenSSL library used by the official Windows builds has been upgraded to OpenSSL 4. +- Standard: + . stream_select() no longer caps the number of sockets at FD_SETSIZE; the + winsock fd_set now grows on demand, so socket-heavy code (event loops, + servers) behaves like it does on POSIX. Selecting on more than 64 pipe or + file handles no longer crashes the process; it fails with a warning instead, + as that path is still bounded by the WaitForMultipleObjects() limit. + ======================================== 13. Other Changes ======================================== diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 5c5cd4c6ebb1..effb1b7f9c2c 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -799,7 +799,7 @@ static int stream_array_from_fd_bigset(zval *stream_array, const fd_bigset *fds) #else /* PHP_WIN32 */ /* {{{ Windows keeps the original native fd_set based implementation for now (capped at FD_SETSIZE); see the * comment above the fd_bigset typedef for why. */ -static int stream_array_to_fd_set(const HashTable *stream_array, fd_set *fds, php_socket_t *max_fd) +static int stream_array_to_fd_set(const HashTable *stream_array, php_growable_fd_set *fds, php_socket_t *max_fd) { zval *elem; php_stream *stream; @@ -823,7 +823,7 @@ static int stream_array_to_fd_set(const HashTable *stream_array, fd_set *fds, ph * */ if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != -1) { - PHP_SAFE_FD_SET(this_fd, fds); + php_growable_fd_set_add(fds, (SOCKET)this_fd); if (this_fd > *max_fd) { *max_fd = this_fd; @@ -834,7 +834,7 @@ static int stream_array_to_fd_set(const HashTable *stream_array, fd_set *fds, ph return cnt ? 1 : 0; } -static int stream_array_from_fd_set(zval *stream_array, const fd_set *fds) +static int stream_array_from_fd_set(zval *stream_array, const php_growable_fd_set *fds) { zval *elem, *dest_elem; HashTable *ht; @@ -860,7 +860,7 @@ static int stream_array_from_fd_set(zval *stream_array, const fd_set *fds) * is not displayed. */ if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != SOCK_ERR) { - if (PHP_SAFE_FD_ISSET(this_fd, fds)) { + if (FD_ISSET(this_fd, fds->set)) { if (!key) { dest_elem = zend_hash_index_update(ht, num_ind, elem); } else { @@ -936,7 +936,11 @@ static int stream_array_emulate_read_fd_set(zval *stream_array) FD_BIGSET_FREE(&efds); \ } while (0) #else -# define STREAM_SELECT_FREE_SETS() do {} while (0) +# define STREAM_SELECT_FREE_SETS() do { \ + php_growable_fd_set_destroy(&rfds); \ + php_growable_fd_set_destroy(&wfds); \ + php_growable_fd_set_destroy(&efds); \ +} while (0) #endif /* {{{ Runs the select() system call on the sets of streams with a timeout specified by tv_sec and tv_usec */ @@ -947,7 +951,7 @@ PHP_FUNCTION(stream_select) #ifndef PHP_WIN32 fd_bigset rfds, wfds, efds; #else - fd_set rfds, wfds, efds; + php_growable_fd_set rfds, wfds, efds; #endif php_socket_t max_fd = 0; int retval, sets = 0; @@ -1011,9 +1015,11 @@ PHP_FUNCTION(stream_select) sets += set_count; } #else - FD_ZERO(&rfds); - FD_ZERO(&wfds); - FD_ZERO(&efds); + /* Size the growable sets to each array's element count (clamped to at least + * FD_SETSIZE); they still grow on demand, so this is only a starting guess. */ + php_growable_fd_set_init(&rfds, r_array != NULL ? zend_hash_num_elements(Z_ARR_P(r_array)) : 0); + php_growable_fd_set_init(&wfds, w_array != NULL ? zend_hash_num_elements(Z_ARR_P(w_array)) : 0); + php_growable_fd_set_init(&efds, e_array != NULL ? zend_hash_num_elements(Z_ARR_P(e_array)) : 0); if (r_array != NULL) { set_count = stream_array_to_fd_set(Z_ARR_P(r_array), &rfds, &max_fd); @@ -1116,7 +1122,7 @@ PHP_FUNCTION(stream_select) retval = php_select(max_fd + 1, (fd_set *) rfds.fds_bits, (fd_set *) wfds.fds_bits, (fd_set *) efds.fds_bits, tv_p); #else - retval = php_select(max_fd + 1, &rfds, &wfds, &efds, tv_p); + retval = php_select(max_fd + 1, rfds.set, wfds.set, efds.set, tv_p); #endif php_stream_error_operation_end(context); diff --git a/ext/standard/tests/streams/stream_select_win32_many_sockets.phpt b/ext/standard/tests/streams/stream_select_win32_many_sockets.phpt new file mode 100644 index 000000000000..1a37ed7977ed --- /dev/null +++ b/ext/standard/tests/streams/stream_select_win32_many_sockets.phpt @@ -0,0 +1,44 @@ +--TEST-- +stream_select(): more than FD_SETSIZE sockets on Windows (growable fd_set) +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) diff --git a/ext/standard/tests/streams/stream_select_win32_max_handles.phpt b/ext/standard/tests/streams/stream_select_win32_max_handles.phpt new file mode 100644 index 000000000000..86203fccab72 --- /dev/null +++ b/ext/standard/tests/streams/stream_select_win32_max_handles.phpt @@ -0,0 +1,40 @@ +--TEST-- +stream_select(): >64 non-socket handles fail gracefully on Windows (no stack overflow) +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +Warning: stream_select(): Unable to select [%d]: %s (max_fd=%d) in %s on line %d +bool(false) +--CLEAN-- + diff --git a/main/php_network.h b/main/php_network.h index c93a519911f0..94520d7555f8 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -248,6 +248,75 @@ static inline bool _php_check_fd_setsize(php_socket_t *max_fd, int setsize) # define PHP_SAFE_MAX_FD(m, n) _php_check_fd_setsize(&m, n) #endif +#ifdef PHP_WIN32 +/* {{{ Growable winsock fd_set. + * + * On Windows fd_set is a *packed array* of SOCKETs ({ u_int fd_count; SOCKET + * fd_array[FD_SETSIZE]; }), not a bitset, so the traditional FD_SETSIZE ceiling + * caps the *number* of sockets rather than their descriptor value. + * php_growable_fd_set lifts that limit by keeping the winsock fd_set on the heap + * and doubling fd_array on demand. Because select()/FD_ISSET/FD_ZERO only ever + * read fd_count, `set` can be handed to them directly at any size. Windows only; + * the POSIX build grows a bitset (fd_bigset) instead - see + * ext/standard/streamsfuncs.c. */ +typedef struct { + u_int capacity; /* number of SOCKET slots backing set->fd_array */ + fd_set *set; /* heap block laid out as a winsock fd_set of `capacity` slots */ +} php_growable_fd_set; + +# define PHP_GROWABLE_FD_SET_ALLOC_SIZE(cap) \ + (offsetof(fd_set, fd_array) + (size_t)(cap) * sizeof(SOCKET)) + +static zend_always_inline void php_growable_fd_set_init(php_growable_fd_set *s, u_int capacity) +{ + if (capacity < FD_SETSIZE) { + capacity = FD_SETSIZE; + } + s->capacity = capacity; + s->set = (fd_set *) pemalloc(PHP_GROWABLE_FD_SET_ALLOC_SIZE(capacity), 1); + s->set->fd_count = 0; +} + +static zend_always_inline void php_growable_fd_set_destroy(php_growable_fd_set *s) +{ + if (s->set) { + pefree(s->set, 1); + s->set = NULL; + } + s->capacity = 0; +} + +static zend_always_inline void php_growable_fd_set_zero(php_growable_fd_set *s) +{ + s->set->fd_count = 0; +} + +static zend_always_inline void php_growable_fd_set_reserve(php_growable_fd_set *s, u_int needed) +{ + if (needed > s->capacity) { + do { + s->capacity *= 2; + } while (needed > s->capacity); + s->set = (fd_set *) perealloc(s->set, PHP_GROWABLE_FD_SET_ALLOC_SIZE(s->capacity), 1); + } +} + +static zend_always_inline void php_growable_fd_set_add(php_growable_fd_set *s, SOCKET fd) +{ + /* fds within a single stream_select() array are already unique, so we skip + * the O(n) duplicate scan that winsock's FD_SET macro performs. */ + php_growable_fd_set_reserve(s, s->set->fd_count + 1); + s->set->fd_array[s->set->fd_count++] = fd; +} + +static zend_always_inline void php_growable_fd_set_copy(php_growable_fd_set *dst, const php_growable_fd_set *src) +{ + php_growable_fd_set_reserve(dst, src->set->fd_count); + memcpy(dst->set, src->set, PHP_GROWABLE_FD_SET_ALLOC_SIZE(src->set->fd_count)); +} +/* }}} */ +#endif + #define PHP_SOCK_CHUNK_SIZE 8192 diff --git a/win32/select.c b/win32/select.c index 988f037d7a4c..2dde8db37d27 100644 --- a/win32/select.c +++ b/win32/select.c @@ -36,11 +36,11 @@ PHPAPI int php_select(php_socket_t max_fd, fd_set *rfds, fd_set *wfds, fd_set *e int handle_slot_to_fd[MAXIMUM_WAIT_OBJECTS]; int n_handles = 0, i; int num_read_pipes = 0; - fd_set sock_read, sock_write, sock_except; - fd_set aread, awrite, aexcept; + php_growable_fd_set sock_read = {0}, sock_write = {0}, sock_except = {0}; + php_growable_fd_set aread = {0}, awrite = {0}, aexcept = {0}; int sock_max_fd = -1; struct timeval tvslice; - int retcode; + int retcode = -1; /* As max_fd is unsigned, non socket might overflow. */ if (max_fd > (php_socket_t)INT_MAX) { @@ -57,9 +57,9 @@ PHPAPI int php_select(php_socket_t max_fd, fd_set *rfds, fd_set *wfds, fd_set *e ms_total += tv->tv_usec / 1000; } - FD_ZERO(&sock_read); - FD_ZERO(&sock_write); - FD_ZERO(&sock_except); + php_growable_fd_set_init(&sock_read, FD_SETSIZE); + php_growable_fd_set_init(&sock_write, FD_SETSIZE); + php_growable_fd_set_init(&sock_except, FD_SETSIZE); /* build an array of handles for non-sockets */ for (i = 0; (uint32_t)i < max_fd; i++) { @@ -70,21 +70,32 @@ PHPAPI int php_select(php_socket_t max_fd, fd_set *rfds, fd_set *wfds, fd_set *e if (getsockopt((SOCKET)i, SOL_SOCKET, SO_TYPE, (char*)&_type, &_len) == 0 || WSAGetLastError() != WSAENOTSOCK) { /* socket */ if (SAFE_FD_ISSET(i, rfds)) { - FD_SET((uint32_t)i, &sock_read); + php_growable_fd_set_add(&sock_read, (SOCKET)(uintptr_t)i); } if (SAFE_FD_ISSET(i, wfds)) { - FD_SET((uint32_t)i, &sock_write); + php_growable_fd_set_add(&sock_write, (SOCKET)(uintptr_t)i); } if (SAFE_FD_ISSET(i, efds)) { - FD_SET((uint32_t)i, &sock_except); + php_growable_fd_set_add(&sock_except, (SOCKET)(uintptr_t)i); } if (i > sock_max_fd) { sock_max_fd = i; } } else { - handles[n_handles] = (HANDLE)(uintptr_t)_get_osfhandle(i); - if (handles[n_handles] != INVALID_HANDLE_VALUE) { - if (SAFE_FD_ISSET(i, rfds) && GetFileType(handles[n_handles]) == FILE_TYPE_PIPE) { + HANDLE handle = (HANDLE)(uintptr_t)_get_osfhandle(i); + if (handle != INVALID_HANDLE_VALUE) { + if (n_handles >= MAXIMUM_WAIT_OBJECTS) { + /* WaitForMultipleObjects() cannot wait on more than + * MAXIMUM_WAIT_OBJECTS (64) handles at once. Fail + * gracefully (the caller turns -1 into a warning and + * false) instead of overflowing the fixed-size + * handles[]/handle_slot_to_fd[] stack arrays. */ + errno = EINVAL; + retcode = -1; + goto cleanup; + } + handles[n_handles] = handle; + if (SAFE_FD_ISSET(i, rfds) && GetFileType(handle) == FILE_TYPE_PIPE) { num_read_pipes++; } handle_slot_to_fd[n_handles] = i; @@ -95,32 +106,39 @@ PHPAPI int php_select(php_socket_t max_fd, fd_set *rfds, fd_set *wfds, fd_set *e } if (n_handles == 0) { - /* plain sockets only - let winsock handle the whole thing */ - return select(-1, rfds, wfds, efds, tv); + /* plain sockets only - let winsock handle the whole thing. rfds/wfds/efds + * are growable sets, so this is no longer bounded by FD_SETSIZE. */ + retcode = select(-1, rfds, wfds, efds, tv); + goto cleanup; } /* mixture of handles and sockets; lets multiplex between * winsock and waiting on the handles */ - FD_ZERO(&aread); - FD_ZERO(&awrite); - FD_ZERO(&aexcept); + php_growable_fd_set_init(&aread, sock_read.set->fd_count); + php_growable_fd_set_init(&awrite, sock_write.set->fd_count); + php_growable_fd_set_init(&aexcept, sock_except.set->fd_count); limit = GetTickCount64() + ms_total; do { retcode = 0; if (sock_max_fd >= 0) { - /* overwrite the zero'd sets here; the select call - * will clear those that are not active */ - aread = sock_read; - awrite = sock_write; - aexcept = sock_except; + /* refresh the working copies; the select call will clear the fds + * that are not active. memcpy (via _copy) instead of struct + * assignment because the sets are dynamically sized. */ + php_growable_fd_set_copy(&aread, &sock_read); + php_growable_fd_set_copy(&awrite, &sock_write); + php_growable_fd_set_copy(&aexcept, &sock_except); tvslice.tv_sec = 0; tvslice.tv_usec = 100000; - retcode = select(-1, &aread, &awrite, &aexcept, &tvslice); + retcode = select(-1, aread.set, awrite.set, aexcept.set, &tvslice); + } else { + php_growable_fd_set_zero(&aread); + php_growable_fd_set_zero(&awrite); + php_growable_fd_set_zero(&aexcept); } if (n_handles > 0) { /* check handles */ @@ -150,16 +168,16 @@ PHPAPI int php_select(php_socket_t max_fd, fd_set *rfds, fd_set *wfds, fd_set *e || !PeekNamedPipe(handles[i], NULL, 0, NULL, &avail_read, NULL) || avail_read > 0 ) { - FD_SET((uint32_t)handle_slot_to_fd[i], &aread); + php_growable_fd_set_add(&aread, (SOCKET)(uintptr_t)handle_slot_to_fd[i]); retcode++; } } if (SAFE_FD_ISSET(handle_slot_to_fd[i], wfds)) { - FD_SET((uint32_t)handle_slot_to_fd[i], &awrite); + php_growable_fd_set_add(&awrite, (SOCKET)(uintptr_t)handle_slot_to_fd[i]); retcode++; } if (SAFE_FD_ISSET(handle_slot_to_fd[i], efds)) { - FD_SET((uint32_t)handle_slot_to_fd[i], &aexcept); + php_growable_fd_set_add(&aexcept, (SOCKET)(uintptr_t)handle_slot_to_fd[i]); retcode++; } } @@ -171,15 +189,29 @@ PHPAPI int php_select(php_socket_t max_fd, fd_set *rfds, fd_set *wfds, fd_set *e } } while (retcode == 0 && (ms_total == INFINITE || GetTickCount64() < limit)); + /* Copy the results back into the caller's sets. memcpy (not struct + * assignment) because the sets are dynamically sized; only fd_count + the + * used fd_array entries are written. This never overflows the caller's + * buffer: every fd in a* was present in the corresponding input set, so + * a*->fd_count <= the input fd_count, which the caller's buffer already + * held. */ if (rfds) { - *rfds = aread; + memcpy(rfds, aread.set, PHP_GROWABLE_FD_SET_ALLOC_SIZE(aread.set->fd_count)); } if (wfds) { - *wfds = awrite; + memcpy(wfds, awrite.set, PHP_GROWABLE_FD_SET_ALLOC_SIZE(awrite.set->fd_count)); } if (efds) { - *efds = aexcept; + memcpy(efds, aexcept.set, PHP_GROWABLE_FD_SET_ALLOC_SIZE(aexcept.set->fd_count)); } +cleanup: + php_growable_fd_set_destroy(&sock_read); + php_growable_fd_set_destroy(&sock_write); + php_growable_fd_set_destroy(&sock_except); + php_growable_fd_set_destroy(&aread); + php_growable_fd_set_destroy(&awrite); + php_growable_fd_set_destroy(&aexcept); + return retcode; } From 2fa18f0f975b1510648e5f9451d650613cd8fd9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Tue, 22 Sep 2026 12:57:31 +0000 Subject: [PATCH 18/21] Document the POSIX fd_set limit removal; correct UPGRADING and test wording Add the missing NEWS/UPGRADING entry for the POSIX side of the stream_select() change (the limit is now lifted on POSIX as well as Windows, not just Windows), correct the UPGRADING note that implied POSIX was already unbounded, and trim the max-handles test docblock to a plain description of the behavior it guards. --- NEWS | 9 ++++++--- UPGRADING | 11 ++++++----- .../streams/stream_select_win32_max_handles.phpt | 6 ++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/NEWS b/NEWS index 02b6f045a029..be77e69840f5 100644 --- a/NEWS +++ b/NEWS @@ -95,12 +95,15 @@ PHP NEWS . Fixed three Windows-only proc_open() defects: an uninitialized PROCESS_INFORMATION, an indeterminate comspec pointer after a failed lookup, and an unchecked CreateFileA() failure. (Ilia Alshanetsky) + . Fixed stream_select() aborting the whole call once any watched stream's + descriptor exceeded FD_SETSIZE; it no longer imposes that limit, so watching + many descriptors no longer requires rebuilding with --enable-fd-setsize. + (Frode Børli) + . Fixed stream_select() on Windows capping the number of watched sockets at + FD_SETSIZE; the winsock fd_set now grows on demand. (Frode Børli) . Fixed stream_select() on Windows overflowing a fixed 64-entry stack array (crashing with a stack buffer overrun) when passed more than 64 pipe or file handles; it now fails gracefully instead. (Frode Børli) - . Removed the FD_SETSIZE ceiling on the number of sockets stream_select() can - watch on Windows; the winsock fd_set now grows on demand, matching POSIX. - (Frode Børli) - Zlib: . Fixed inflate_init() dropping the preset dictionary for raw streams with diff --git a/UPGRADING b/UPGRADING index 484b8f3410ee..5c4e0eaff565 100644 --- a/UPGRADING +++ b/UPGRADING @@ -1071,11 +1071,12 @@ PHP 8.6 UPGRADE NOTES to OpenSSL 4. - Standard: - . stream_select() no longer caps the number of sockets at FD_SETSIZE; the - winsock fd_set now grows on demand, so socket-heavy code (event loops, - servers) behaves like it does on POSIX. Selecting on more than 64 pipe or - file handles no longer crashes the process; it fails with a warning instead, - as that path is still bounded by the WaitForMultipleObjects() limit. + . stream_select() no longer fails once a watched descriptor exceeds + FD_SETSIZE. The descriptor set now grows on demand on both POSIX (bitset) + and Windows (winsock fd_set), so code watching many streams works without + rebuilding PHP with --enable-fd-setsize. On Windows, selecting on more than + 64 pipe or file handles no longer crashes; it fails with a warning, as that + path remains bounded by WaitForMultipleObjects(). ======================================== 13. Other Changes diff --git a/ext/standard/tests/streams/stream_select_win32_max_handles.phpt b/ext/standard/tests/streams/stream_select_win32_max_handles.phpt index 86203fccab72..5d0ee495d2fc 100644 --- a/ext/standard/tests/streams/stream_select_win32_max_handles.phpt +++ b/ext/standard/tests/streams/stream_select_win32_max_handles.phpt @@ -10,10 +10,8 @@ if (PHP_OS_FAMILY !== 'Windows') die('skip Windows only'); * through WaitForMultipleObjects(), which cannot wait on more than * MAXIMUM_WAIT_OBJECTS (64) handles at once. php_select() used to push each * handle into a fixed 64-slot stack array with no bounds check, so selecting on - * 65+ file/pipe handles overran the stack and crashed the process with - * STATUS_STACK_BUFFER_OVERRUN (0xC0000409). It must instead fail gracefully - * (warning + false). This test crashes every unpatched PHP build on Windows and - * passes only once the cap is in place. */ + * 65+ file/pipe handles overran the stack. It must instead fail gracefully + * (warning + false). */ $files = []; for ($i = 0; $i < 70; $i++) { $path = __DIR__ . "/stream_select_win32_max_handles_$i.tmp"; From 8a30d889f89ef02f44fa3e62c171628815d70420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Fri, 25 Sep 2026 12:28:15 +0000 Subject: [PATCH 19/21] Test stream_select() past FD_SETSIZE in the write and except sets The existing GH-9590 tests only put descriptors in the read set, but each of the three fd_sets grows independently. Put a descriptor beyond FD_SETSIZE in all three sets (plus a low one in two of them) and check select()'s count of set bits across the sets, the returned arrays, and a write-set-only select. --- ext/standard/tests/streams/gh9590-003.phpt | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 ext/standard/tests/streams/gh9590-003.phpt diff --git a/ext/standard/tests/streams/gh9590-003.phpt b/ext/standard/tests/streams/gh9590-003.phpt new file mode 100644 index 000000000000..513773baa35d --- /dev/null +++ b/ext/standard/tests/streams/gh9590-003.phpt @@ -0,0 +1,67 @@ +--TEST-- +Bug GH-9590 003 (stream_select past FD_SETSIZE: every set grows, and a stream in several sets is counted once per set) +--EXTENSIONS-- +posix +--SKIPIF-- + +--FILE-- + $lo, 'hi' => $hi]; +$w = ['hi' => $hi]; +$e = ['hi' => $hi, 'lo' => $lo]; +var_dump(stream_select($r, $w, $e, 30)); +var_dump(array_keys($r), array_keys($w), $e); + +// A beyond-FD_SETSIZE stream in the write set alone. +$r = $e = null; +$w = [$hi]; +var_dump(stream_select($r, $w, $e, 30)); + +?> +--EXPECT-- +int(3) +array(2) { + [0]=> + string(2) "lo" + [1]=> + string(2) "hi" +} +array(1) { + [0]=> + string(2) "hi" +} +array(0) { +} +int(1) +--CLEAN-- + From 925a91d1b4ea8755041a9ba3ec01320b23b67172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Fri, 25 Sep 2026 13:59:39 +0000 Subject: [PATCH 20/21] Test stream_select() on Windows with the write and except sets grown too stream_select_win32_many_sockets.phpt only exercises the read set, but on Windows each of the three fd_sets is a separately grown handle array. Put PHP_FD_SETSIZE + 50 sockets in the read, write and except sets at once and check select()'s count of entries across all sets (each socket is readable and writable, so it counts twice), plus a write-set-only select. Verified on the Windows CI build (x64 ZTS and x86 NTS). --- .../stream_select_win32_multi_set.phpt | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 ext/standard/tests/streams/stream_select_win32_multi_set.phpt diff --git a/ext/standard/tests/streams/stream_select_win32_multi_set.phpt b/ext/standard/tests/streams/stream_select_win32_multi_set.phpt new file mode 100644 index 000000000000..1f1e60a3850f --- /dev/null +++ b/ext/standard/tests/streams/stream_select_win32_multi_set.phpt @@ -0,0 +1,51 @@ +--TEST-- +stream_select(): more than FD_SETSIZE sockets in the write and except sets on Windows (each set grows) +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +int(0) +bool(true) From 35fa4da78d554d4b156bb451d9126c770d01b12d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frode=20B=C3=B8rli?= Date: Tue, 29 Sep 2026 16:02:53 +0000 Subject: [PATCH 21/21] Unify the growable fd set for POSIX and Windows Address review: stream_select() uses one php_growable_fd_set interface (init, reserve, add, isset, destroy) on every platform, with a bitset on POSIX and a SOCKET array on Windows, as inline functions in php_network.h. - Start at FD_SETSIZE instead of sizing from the open file limit. - The POSIX bitset is indexed by long words, as select() reads it; the byte indexing was only correct on little-endian systems. - Drop the PHP_SAFE_MAX_FD() check: on POSIX it was the limit this removes, and on Windows it could not fail (set counts are 0 or 1). - One cleanup path in stream_select(); declarations moved to their use. - GH-9590 tests keep the current hard RLIMIT_NOFILE: raising it to unlimited fails even for root on Linux, so they were always skipped. --- ext/standard/streamsfuncs.c | 348 +++------------------ ext/standard/tests/streams/gh9590-001.phpt | 7 +- ext/standard/tests/streams/gh9590-002.phpt | 7 +- ext/standard/tests/streams/gh9590-003.phpt | 7 +- main/php_network.h | 108 +++++-- 5 files changed, 136 insertions(+), 341 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 2ab657cd6117..897e773ae0e2 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -647,158 +647,7 @@ PHP_FUNCTION(stream_clear_errors) php_stream_error_clear_stored(); } -#ifndef PHP_WIN32 -/* {{{ fd_bigset related macros and typedefs to remove the FD_SETSIZE limitation from stream_select() on - * POSIX-like systems. select() itself has no such limit there; only the fixed-size fd_set imposes the - * traditional 1024 descriptor ceiling, so we build a bitset that grows on demand instead. - * - * Windows keeps using the native fd_set (see stream_array_to_fd_set() below): there, fd_set is an array of - * SOCKET handles capped at FD_SETSIZE rather than a bitset indexed by descriptor number, and growing it - * requires a different approach that is left for a follow-up. */ -typedef struct { - char *fds_bits; - size_t size; -} fd_bigset; - -static void fd_bigset_double_size(fd_bigset *set); - -#define FD_BIGSET_ENSURE_CAPACITY(fd, set) \ - while (UNEXPECTED(((fd) / 8 >= (set)->size))) { \ - fd_bigset_double_size(set); \ - } -/* select() operates on whole `long`-sized words (see e.g. Linux's FDS_BYTES()/NFDBITS): - * it always reads/writes size-rounded-up-to-sizeof(long) bytes for a given nfds, even - * though the traditional fd_set is only ever created at that exact alignment (FD_SETSIZE - * is a multiple of 8*sizeof(long)). Since our size is derived from an arbitrary fd count, - * we must round up to a whole `long` ourselves, or select() will read/write past our - * heap allocation into adjacent memory whenever num_fds isn't a multiple of 8*sizeof(long). */ -#define FD_BIGSET_ZERO(set, num_fds) do { \ - size_t num_longs = ((size_t) (num_fds) + (8 * sizeof(long)) - 1) / (8 * sizeof(long)); \ - (set)->size = num_longs * sizeof(long); \ - (set)->fds_bits = (char *) ecalloc((set)->size, sizeof(char)); \ -} while (0) -#define FD_BIGSET_SET(fd, set) do { \ - FD_BIGSET_ENSURE_CAPACITY(fd, set) \ - (set)->fds_bits[(fd) / 8] |= (1 << ((fd) % 8)); \ -} while (0) -#define FD_BIGSET_ISSET(fd, set) (((fd) / 8 < (set)->size) ? ((set)->fds_bits[(fd) / 8] & (1 << ((fd) % 8))) : 0) -#define FD_BIGSET_CLR(fd, set) do { \ - if ((fd) / 8 < (set)->size) { \ - (set)->fds_bits[(fd) / 8] &= ~(1 << ((fd) % 8)); \ - } \ -} while (0) -#define FD_BIGSET_FREE(set) do { \ - if ((set)->fds_bits) { \ - efree((set)->fds_bits); \ - (set)->fds_bits = NULL; \ - } \ - (set)->size = 0; \ -} while (0) - -/* {{{ Doubles the backing storage of an fd_bigset */ -static void fd_bigset_double_size(fd_bigset *set) -{ - size_t old_size, new_size; - - ZEND_ASSERT(set && set->fds_bits); - - old_size = set->size; - new_size = old_size * 2; - - /* Reallocate memory block with twice the size of the original */ - set->fds_bits = erealloc(set->fds_bits, new_size); - - /* Zero out the extra capacity */ - memset(set->fds_bits + old_size, 0, new_size - old_size); - - set->size = new_size; -} -/* }}} */ - -static int stream_array_to_fd_bigset(const HashTable *stream_array, fd_bigset *set, php_socket_t *max_fd) -{ - zval *elem; - php_stream *stream; - int cnt = 0; - - ZEND_HASH_FOREACH_VAL(stream_array, elem) { - /* Temporary int fd is needed for the STREAM data type on windows, passing this_fd directly to php_stream_cast() - would eventually bring a wrong result on x64. php_stream_cast() casts to int internally, and this will leave - the higher bits of a SOCKET variable uninitialized on systems with little endian. */ - php_socket_t this_fd; - - ZVAL_DEREF(elem); - php_stream_from_zval_no_verify(stream, elem); - if (stream == NULL) { - continue; - } - /* get the fd. - * NB: Most other code will NOT use the PHP_STREAM_CAST_INTERNAL flag - * when casting. It is only used here so that the buffered data warning - * is not displayed. - * */ - if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != -1) { - FD_BIGSET_SET(this_fd, set); - if (this_fd > *max_fd) { - *max_fd = this_fd; - } - cnt++; - } - } ZEND_HASH_FOREACH_END(); - - return cnt ? 1 : 0; -} - -static int stream_array_from_fd_bigset(zval *stream_array, const fd_bigset *fds) -{ - zval *elem, *dest_elem; - HashTable *ht; - php_stream *stream; - int ret = 0; - zend_string *key; - zend_ulong num_ind; - - ZEND_ASSERT(Z_TYPE_P(stream_array) == IS_ARRAY); - ht = zend_new_array(zend_hash_num_elements(Z_ARRVAL_P(stream_array))); - - ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(stream_array), num_ind, key, elem) { - php_socket_t this_fd; - - ZVAL_DEREF(elem); - php_stream_from_zval_no_verify(stream, elem); - if (stream == NULL) { - continue; - } - - /* get the fd. - * NB: Most other code will NOT use the PHP_STREAM_CAST_INTERNAL flag - * when casting. It is only used here so that the buffered data warning - * is not displayed. - */ - if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != SOCK_ERR) { - if (FD_BIGSET_ISSET(this_fd, fds)) { - if (!key) { - dest_elem = zend_hash_index_update(ht, num_ind, elem); - } else { - dest_elem = zend_hash_update(ht, key, elem); - } - - zval_add_ref(dest_elem); - ret++; - } - } - } ZEND_HASH_FOREACH_END(); - - /* destroy old array and add new one */ - zval_ptr_dtor(stream_array); - ZVAL_ARR(stream_array, ht); - - return ret; -} -/* }}} */ -#else /* PHP_WIN32 */ -/* {{{ Windows keeps the original native fd_set based implementation for now (capped at FD_SETSIZE); see the - * comment above the fd_bigset typedef for why. */ +/* {{{ stream_select related functions */ static int stream_array_to_fd_set(const HashTable *stream_array, php_growable_fd_set *fds, php_socket_t *max_fd) { zval *elem; @@ -823,7 +672,7 @@ static int stream_array_to_fd_set(const HashTable *stream_array, php_growable_fd * */ if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != -1) { - php_growable_fd_set_add(fds, (SOCKET)this_fd); + php_growable_fd_set_add(fds, this_fd); if (this_fd > *max_fd) { *max_fd = this_fd; @@ -836,17 +685,12 @@ static int stream_array_to_fd_set(const HashTable *stream_array, php_growable_fd static int stream_array_from_fd_set(zval *stream_array, const php_growable_fd_set *fds) { - zval *elem, *dest_elem; - HashTable *ht; - php_stream *stream; - int ret = 0; - zend_string *key; - zend_ulong num_ind; - ZEND_ASSERT(Z_TYPE_P(stream_array) == IS_ARRAY); - ht = zend_new_array(zend_hash_num_elements(Z_ARRVAL_P(stream_array))); + HashTable *ht = zend_new_array(zend_hash_num_elements(Z_ARRVAL_P(stream_array))); + int ret = 0; - ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(stream_array), num_ind, key, elem) { + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(stream_array), zend_ulong num_ind, zend_string *key, zval *elem) { + php_stream *stream; php_socket_t this_fd; ZVAL_DEREF(elem); @@ -860,7 +704,8 @@ static int stream_array_from_fd_set(zval *stream_array, const php_growable_fd_se * is not displayed. */ if (SUCCESS == php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) && this_fd != SOCK_ERR) { - if (FD_ISSET(this_fd, fds->set)) { + if (php_growable_fd_set_isset(fds, this_fd)) { + zval *dest_elem; if (!key) { dest_elem = zend_hash_index_update(ht, num_ind, elem); } else { @@ -879,8 +724,6 @@ static int stream_array_from_fd_set(zval *stream_array, const php_growable_fd_se return ret; } -/* }}} */ -#endif /* PHP_WIN32 */ static int stream_array_emulate_read_fd_set(zval *stream_array) { @@ -929,37 +772,13 @@ static int stream_array_emulate_read_fd_set(zval *stream_array) } /* }}} */ -#ifndef PHP_WIN32 -# define STREAM_SELECT_FREE_SETS() do { \ - FD_BIGSET_FREE(&rfds); \ - FD_BIGSET_FREE(&wfds); \ - FD_BIGSET_FREE(&efds); \ -} while (0) -#else -# define STREAM_SELECT_FREE_SETS() do { \ - php_growable_fd_set_destroy(&rfds); \ - php_growable_fd_set_destroy(&wfds); \ - php_growable_fd_set_destroy(&efds); \ -} while (0) -#endif - /* {{{ Runs the select() system call on the sets of streams with a timeout specified by tv_sec and tv_usec */ PHP_FUNCTION(stream_select) { zval *r_array, *w_array, *e_array, *zcontext = NULL; - struct timeval tv, *tv_p = NULL; -#ifndef PHP_WIN32 - fd_bigset rfds, wfds, efds; -#else - php_growable_fd_set rfds, wfds, efds; -#endif - php_socket_t max_fd = 0; - int retval, sets = 0; zend_long sec, usec = 0; bool secnull; bool usecnull = 1; - int set_count, max_set_count = 0; - php_stream_context *context = NULL; ZEND_PARSE_PARAMETERS_START(4, 6) Z_PARAM_ARRAY_EX2(r_array, 1, 1, 0) @@ -972,115 +791,51 @@ PHP_FUNCTION(stream_select) ZEND_PARSE_PARAMETERS_END(); php_stream_error_operation_begin(); - context = php_stream_context_from_zval(zcontext, 0); - -#ifndef PHP_WIN32 - /* Get max possible descriptor count for the initial allocation; the fd_bigset grows on - * demand regardless, so this is only sizing a starting guess, not a hard limit. */ - long max_fds = sysconf(_SC_OPEN_MAX); - if (max_fds <= 0) { - max_fds = 131072; - } - - /* Cap the initial allocation at 131072; fd_bigset will be dynamically resized if needed. */ - if (max_fds > 131072) { - max_fds = 131072; - } - - FD_BIGSET_ZERO(&rfds, max_fds); - FD_BIGSET_ZERO(&wfds, max_fds); - FD_BIGSET_ZERO(&efds, max_fds); - - if (r_array != NULL) { - set_count = stream_array_to_fd_bigset(Z_ARR_P(r_array), &rfds, &max_fd); - if (set_count > max_set_count) { - max_set_count = set_count; - } - sets += set_count; - } + php_stream_context *context = php_stream_context_from_zval(zcontext, 0); - if (w_array != NULL) { - set_count = stream_array_to_fd_bigset(Z_ARR_P(w_array), &wfds, &max_fd); - if (set_count > max_set_count) { - max_set_count = set_count; - } - sets += set_count; - } - - if (e_array != NULL) { - set_count = stream_array_to_fd_bigset(Z_ARR_P(e_array), &efds, &max_fd); - if (set_count > max_set_count) { - max_set_count = set_count; - } - sets += set_count; - } -#else - /* Size the growable sets to each array's element count (clamped to at least - * FD_SETSIZE); they still grow on demand, so this is only a starting guess. */ - php_growable_fd_set_init(&rfds, r_array != NULL ? zend_hash_num_elements(Z_ARR_P(r_array)) : 0); - php_growable_fd_set_init(&wfds, w_array != NULL ? zend_hash_num_elements(Z_ARR_P(w_array)) : 0); - php_growable_fd_set_init(&efds, e_array != NULL ? zend_hash_num_elements(Z_ARR_P(e_array)) : 0); + /* The sets grow as streams are added, past FD_SETSIZE */ + php_growable_fd_set rfds, wfds, efds; + php_growable_fd_set_init(&rfds, FD_SETSIZE); + php_growable_fd_set_init(&wfds, FD_SETSIZE); + php_growable_fd_set_init(&efds, FD_SETSIZE); + php_socket_t max_fd = 0; + int sets = 0; if (r_array != NULL) { - set_count = stream_array_to_fd_set(Z_ARR_P(r_array), &rfds, &max_fd); - if (set_count > max_set_count) { - max_set_count = set_count; - } - sets += set_count; + sets += stream_array_to_fd_set(Z_ARR_P(r_array), &rfds, &max_fd); } - if (w_array != NULL) { - set_count = stream_array_to_fd_set(Z_ARR_P(w_array), &wfds, &max_fd); - if (set_count > max_set_count) { - max_set_count = set_count; - } - sets += set_count; + sets += stream_array_to_fd_set(Z_ARR_P(w_array), &wfds, &max_fd); } - if (e_array != NULL) { - set_count = stream_array_to_fd_set(Z_ARR_P(e_array), &efds, &max_fd); - if (set_count > max_set_count) { - max_set_count = set_count; - } - sets += set_count; + sets += stream_array_to_fd_set(Z_ARR_P(e_array), &efds, &max_fd); } -#endif if (!sets) { php_stream_error_operation_end(context); zend_value_error("No stream arrays were passed"); - STREAM_SELECT_FREE_SETS(); - RETURN_THROWS(); - } - -#ifndef PHP_WIN32 - if (max_set_count == 0) { - RETURN_FALSE; - } -#else - if (!PHP_SAFE_MAX_FD(max_fd, max_set_count)) { - RETURN_FALSE; + goto cleanup; } -#endif - if (secnull && !usecnull && usec != 0) { - php_stream_error_operation_end(context); - zend_argument_value_error(5, "must be null when argument #4 ($seconds) is null"); - STREAM_SELECT_FREE_SETS(); - RETURN_THROWS(); + if (secnull && !usecnull) { + if (usec != 0) { + php_stream_error_operation_end(context); + zend_argument_value_error(5, "must be null when argument #4 ($seconds) is null"); + goto cleanup; + } } + /* If seconds is not set to null, build the timeval, else we wait indefinitely */ + struct timeval tv, *tv_p = NULL; if (!secnull) { if (sec < 0) { php_stream_error_operation_end(context); zend_argument_value_error(4, "must be greater than or equal to 0"); - STREAM_SELECT_FREE_SETS(); - RETURN_THROWS(); + goto cleanup; } else if (usec < 0) { php_stream_error_operation_end(context); zend_argument_value_error(5, "must be greater than or equal to 0"); - STREAM_SELECT_FREE_SETS(); - RETURN_THROWS(); + goto cleanup; } /* Windows, Solaris and BSD do not like microsecond values which are >= 1 sec */ @@ -1092,6 +847,7 @@ PHP_FUNCTION(stream_select) /* slight hack to support buffered data; if there is data sitting in the * read buffer of any of the streams in the read array, let's pretend * that we selected, but return only the readable sockets */ + int retval; if (r_array != NULL) { retval = stream_array_emulate_read_fd_set(r_array); if (retval > 0) { @@ -1104,50 +860,38 @@ PHP_FUNCTION(stream_select) zval_ptr_dtor(e_array); ZVAL_EMPTY_ARRAY(e_array); } - STREAM_SELECT_FREE_SETS(); - RETURN_LONG(retval); + RETVAL_LONG(retval); + goto cleanup; } } #ifndef PHP_WIN32 - /* Ensure all bitmaps are the same size (one or more may have been dynamically resized) */ - { - size_t largest = rfds.size; - if (largest < wfds.size) largest = wfds.size; - if (largest < efds.size) largest = efds.size; - while (rfds.size < largest) fd_bigset_double_size(&rfds); - while (wfds.size < largest) fd_bigset_double_size(&wfds); - while (efds.size < largest) fd_bigset_double_size(&efds); - } - - retval = php_select(max_fd + 1, (fd_set *) rfds.fds_bits, (fd_set *) wfds.fds_bits, (fd_set *) efds.fds_bits, tv_p); -#else - retval = php_select(max_fd + 1, rfds.set, wfds.set, efds.set, tv_p); + /* select() reads max_fd + 1 bits of each set */ + php_growable_fd_set_reserve(&rfds, max_fd + 1); + php_growable_fd_set_reserve(&wfds, max_fd + 1); + php_growable_fd_set_reserve(&efds, max_fd + 1); #endif + retval = php_select(max_fd + 1, rfds.set, wfds.set, efds.set, tv_p); php_stream_error_operation_end(context); if (retval == -1) { php_error_docref(NULL, E_WARNING, "Unable to select [%d]: %s (max_fd=" PHP_SOCKET_FMT ")", - errno, strerror(errno), max_fd); - STREAM_SELECT_FREE_SETS(); - RETURN_FALSE; + errno, strerror(errno), max_fd); + RETVAL_FALSE; + goto cleanup; } -#ifndef PHP_WIN32 - if (r_array != NULL) stream_array_from_fd_bigset(r_array, &rfds); - if (w_array != NULL) stream_array_from_fd_bigset(w_array, &wfds); - if (e_array != NULL) stream_array_from_fd_bigset(e_array, &efds); -#else if (r_array != NULL) stream_array_from_fd_set(r_array, &rfds); if (w_array != NULL) stream_array_from_fd_set(w_array, &wfds); if (e_array != NULL) stream_array_from_fd_set(e_array, &efds); -#endif - STREAM_SELECT_FREE_SETS(); + RETVAL_LONG(retval); - RETURN_LONG(retval); +cleanup: + php_growable_fd_set_destroy(&rfds); + php_growable_fd_set_destroy(&wfds); + php_growable_fd_set_destroy(&efds); } -#undef STREAM_SELECT_FREE_SETS /* }}} */ /* {{{ stream_context related functions */ diff --git a/ext/standard/tests/streams/gh9590-001.phpt b/ext/standard/tests/streams/gh9590-001.phpt index d14247a7cc59..e2eedbff8829 100644 --- a/ext/standard/tests/streams/gh9590-001.phpt +++ b/ext/standard/tests/streams/gh9590-001.phpt @@ -5,7 +5,9 @@ FD_SETSIZE descriptor limit on platforms where that limit has been lifted) posix --SKIPIF-- fd_array */ - fd_set *set; /* heap block laid out as a winsock fd_set of `capacity` slots */ + u_int capacity; /* SOCKET slots in set->fd_array */ + fd_set *set; } php_growable_fd_set; # define PHP_GROWABLE_FD_SET_ALLOC_SIZE(cap) \ @@ -269,14 +266,45 @@ typedef struct { static zend_always_inline void php_growable_fd_set_init(php_growable_fd_set *s, u_int capacity) { - if (capacity < FD_SETSIZE) { - capacity = FD_SETSIZE; + s->capacity = MAX(capacity, FD_SETSIZE); + s->set = (fd_set *) pemalloc(PHP_GROWABLE_FD_SET_ALLOC_SIZE(s->capacity), 1); + s->set->fd_count = 0; +} + +/* Makes room for capacity sockets */ +static zend_always_inline void php_growable_fd_set_reserve(php_growable_fd_set *s, u_int capacity) +{ + if (capacity > s->capacity) { + do { + s->capacity *= 2; + } while (capacity > s->capacity); + s->set = (fd_set *) perealloc(s->set, PHP_GROWABLE_FD_SET_ALLOC_SIZE(s->capacity), 1); } - s->capacity = capacity; - s->set = (fd_set *) pemalloc(PHP_GROWABLE_FD_SET_ALLOC_SIZE(capacity), 1); +} + +static zend_always_inline void php_growable_fd_set_add(php_growable_fd_set *s, php_socket_t fd) +{ + /* Without the duplicate scan of winsock's FD_SET(): callers add each socket once */ + php_growable_fd_set_reserve(s, s->set->fd_count + 1); + s->set->fd_array[s->set->fd_count++] = fd; +} + +static zend_always_inline bool php_growable_fd_set_isset(const php_growable_fd_set *s, php_socket_t fd) +{ + return FD_ISSET(fd, s->set); +} + +static zend_always_inline void php_growable_fd_set_zero(php_growable_fd_set *s) +{ s->set->fd_count = 0; } +static zend_always_inline void php_growable_fd_set_copy(php_growable_fd_set *dst, const php_growable_fd_set *src) +{ + php_growable_fd_set_reserve(dst, src->set->fd_count); + memcpy(dst->set, src->set, PHP_GROWABLE_FD_SET_ALLOC_SIZE(src->set->fd_count)); +} + static zend_always_inline void php_growable_fd_set_destroy(php_growable_fd_set *s) { if (s->set) { @@ -285,36 +313,50 @@ static zend_always_inline void php_growable_fd_set_destroy(php_growable_fd_set * } s->capacity = 0; } +#else +typedef struct { + size_t size; /* bytes in set */ + fd_set *set; +} php_growable_fd_set; -static zend_always_inline void php_growable_fd_set_zero(php_growable_fd_set *s) +# define PHP_GROWABLE_FD_SET_WORD_BITS (CHAR_BIT * sizeof(unsigned long)) +/* select() operates on whole long-sized words so the size must be a multiple of sizeof(long) */ +# define PHP_GROWABLE_FD_SET_SIZE(nfds) (ZEND_MM_ALIGNED_SIZE_EX((size_t)(nfds), PHP_GROWABLE_FD_SET_WORD_BITS) / CHAR_BIT) + +static zend_always_inline void php_growable_fd_set_init(php_growable_fd_set *s, unsigned int capacity) { - s->set->fd_count = 0; + s->size = PHP_GROWABLE_FD_SET_SIZE(capacity); + s->set = (fd_set *) ecalloc(1, s->size); } -static zend_always_inline void php_growable_fd_set_reserve(php_growable_fd_set *s, u_int needed) +/* Makes room for the descriptors below capacity */ +static zend_always_inline void php_growable_fd_set_reserve(php_growable_fd_set *s, unsigned int capacity) { - if (needed > s->capacity) { - do { - s->capacity *= 2; - } while (needed > s->capacity); - s->set = (fd_set *) perealloc(s->set, PHP_GROWABLE_FD_SET_ALLOC_SIZE(s->capacity), 1); + size_t size = PHP_GROWABLE_FD_SET_SIZE(capacity); + if (size > s->size) { + size_t old_size = s->size; + s->size = MAX(size, old_size * 2); + s->set = (fd_set *) erealloc(s->set, s->size); + memset((char *) s->set + old_size, 0, s->size - old_size); } } -static zend_always_inline void php_growable_fd_set_add(php_growable_fd_set *s, SOCKET fd) +static zend_always_inline void php_growable_fd_set_add(php_growable_fd_set *s, php_socket_t fd) { - /* fds within a single stream_select() array are already unique, so we skip - * the O(n) duplicate scan that winsock's FD_SET macro performs. */ - php_growable_fd_set_reserve(s, s->set->fd_count + 1); - s->set->fd_array[s->set->fd_count++] = fd; + php_growable_fd_set_reserve(s, fd + 1); + ((unsigned long *) s->set)[fd / PHP_GROWABLE_FD_SET_WORD_BITS] |= 1UL << (fd % PHP_GROWABLE_FD_SET_WORD_BITS); } -static zend_always_inline void php_growable_fd_set_copy(php_growable_fd_set *dst, const php_growable_fd_set *src) +static zend_always_inline bool php_growable_fd_set_isset(const php_growable_fd_set *s, php_socket_t fd) { - php_growable_fd_set_reserve(dst, src->set->fd_count); - memcpy(dst->set, src->set, PHP_GROWABLE_FD_SET_ALLOC_SIZE(src->set->fd_count)); + return (size_t) fd < s->size * CHAR_BIT + && (((const unsigned long *) s->set)[fd / PHP_GROWABLE_FD_SET_WORD_BITS] >> (fd % PHP_GROWABLE_FD_SET_WORD_BITS)) & 1; +} + +static zend_always_inline void php_growable_fd_set_destroy(php_growable_fd_set *s) +{ + efree(s->set); } -/* }}} */ #endif