Commit 23bb1c9a0ef for php
commit 23bb1c9a0efe7399af86e3b6b2b1679246beabbe
Author: Frode Børli <frodeborli@users.noreply.github.com>
Date: Wed Oct 14 19:35:31 2026 +0200
Fix stream_select() failing unpredictably with many or large descriptors (#14452)
Updated stream_select() so that its not limited by FD_SETSIZE anymore
diff --git a/NEWS b/NEWS
index d64960d02cb..a93cb03247e 100644
--- a/NEWS
+++ b/NEWS
@@ -122,6 +122,15 @@ PHP NEWS
lookup, and an unchecked CreateFileA() failure. (Ilia Alshanetsky)
. Fix persistent stream context lifetime during shutdown (Levi Morrison)
. Fixed bug GH-23780 (unserialize('') fails silently). (timwolla)
+ . 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)
. Improved performance of array_splice() when inserting without removing
elements. (mehmetcansahin)
. Enforce max_filter_count: limit the number of filters that can be chained
diff --git a/UPGRADING b/UPGRADING
index 61f6b492954..fd3efda9df7 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -73,6 +73,14 @@ PHP 8.7 UPGRADE NOTES
5. Changed Functions
========================================
+- Standard:
+ . 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().
+
========================================
6. New Functions
========================================
diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c
index 6502ac279e2..caf5d8f5e44 100644
--- a/ext/standard/streamsfuncs.c
+++ b/ext/standard/streamsfuncs.c
@@ -648,7 +648,7 @@ PHP_FUNCTION(stream_clear_errors)
}
/* {{{ stream_select related functions */
-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;
@@ -672,7 +672,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, this_fd);
if (this_fd > *max_fd) {
*max_fd = this_fd;
@@ -683,19 +683,14 @@ 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;
- 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);
@@ -709,7 +704,8 @@ 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 (php_growable_fd_set_isset(fds, this_fd)) {
+ zval *dest_elem;
if (!key) {
dest_elem = zend_hash_index_update(ht, num_ind, elem);
} else {
@@ -780,15 +776,9 @@ static int stream_array_emulate_read_fd_set(zval *stream_array)
PHP_FUNCTION(stream_select)
{
zval *r_array, *w_array, *e_array, *zcontext = NULL;
- 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;
- php_stream_context *context = NULL;
ZEND_PARSE_PARAMETERS_START(4, 6)
Z_PARAM_ARRAY_EX2(r_array, 1, 1, 0)
@@ -800,62 +790,52 @@ PHP_FUNCTION(stream_select)
Z_PARAM_RESOURCE_OR_NULL(zcontext)
ZEND_PARSE_PARAMETERS_END();
- FD_ZERO(&rfds);
- FD_ZERO(&wfds);
- FD_ZERO(&efds);
-
php_stream_error_operation_begin();
- context = php_stream_context_from_zval(zcontext, 0);
+ php_stream_context *context = php_stream_context_from_zval(zcontext, 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);
}
if (!sets) {
php_stream_error_operation_end(context);
zend_value_error("No stream arrays were passed");
- RETURN_THROWS();
- }
-
- if (!PHP_SAFE_MAX_FD(max_fd, max_set_count)) {
- RETURN_FALSE;
+ goto cleanup;
}
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");
- RETURN_THROWS();
+ 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");
- 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");
- RETURN_THROWS();
+ goto cleanup;
}
/* Windows, Solaris and BSD do not like microsecond values which are >= 1 sec */
@@ -867,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) {
@@ -879,24 +860,37 @@ PHP_FUNCTION(stream_select)
zval_ptr_dtor(e_array);
ZVAL_EMPTY_ARRAY(e_array);
}
- RETURN_LONG(retval);
+ RETVAL_LONG(retval);
+ goto cleanup;
}
}
- retval = php_select(max_fd+1, &rfds, &wfds, &efds, tv_p);
+#ifndef PHP_WIN32
+ /* 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);
- RETURN_FALSE;
+ RETVAL_FALSE;
+ goto cleanup;
}
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);
- RETURN_LONG(retval);
+ RETVAL_LONG(retval);
+
+cleanup:
+ php_growable_fd_set_destroy(&rfds);
+ php_growable_fd_set_destroy(&wfds);
+ php_growable_fd_set_destroy(&efds);
}
/* }}} */
diff --git a/ext/standard/tests/streams/gh9590-001.phpt b/ext/standard/tests/streams/gh9590-001.phpt
index 8f5691af1ad..e2eedbff882 100644
--- a/ext/standard/tests/streams/gh9590-001.phpt
+++ b/ext/standard/tests/streams/gh9590-001.phpt
@@ -1,10 +1,13 @@
--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--
<?php
-if (!function_exists('posix_setrlimit') || !posix_setrlimit(POSIX_RLIMIT_NOFILE, 2048, -1)) {
+// Keep the hard limit: raising it to unlimited fails even for root on Linux (fs.nr_open)
+$hard = function_exists('posix_getrlimit') ? posix_getrlimit(POSIX_RLIMIT_NOFILE)[1] : -1;
+if (!function_exists('posix_setrlimit') || !posix_setrlimit(POSIX_RLIMIT_NOFILE, 2048, is_int($hard) ? $hard : -1)) {
die('skip Failed to set POSIX_RLIMIT_NOFILE');
}
if (PHP_OS_FAMILY === 'Solaris' && PHP_INT_SIZE === 8)
@@ -13,7 +16,8 @@
--FILE--
<?php
-posix_setrlimit(POSIX_RLIMIT_NOFILE, 2048, -1);
+$hard = posix_getrlimit(POSIX_RLIMIT_NOFILE)[1];
+posix_setrlimit(POSIX_RLIMIT_NOFILE, 2048, is_int($hard) ? $hard : -1);
$fds = [];
for ($i = 0; $i < 1023; $i++) {
@@ -21,19 +25,17 @@
}
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--
<?php
for ($i = 0; $i < 1023; $i++) {
diff --git a/ext/standard/tests/streams/gh9590-002.phpt b/ext/standard/tests/streams/gh9590-002.phpt
index 2a9a7c53c67..2c2edf44ddd 100644
--- a/ext/standard/tests/streams/gh9590-002.phpt
+++ b/ext/standard/tests/streams/gh9590-002.phpt
@@ -1,10 +1,14 @@
--TEST--
-Bug GH-9590 002 (stream_select does not abort upon exception or empty valid fd set)
+Bug GH-9590 002 (a custom error handler that turns warnings into exceptions must not be
+spuriously triggered by stream_select() past the traditional FD_SETSIZE limit, on
+platforms where that limit has been lifted)
--EXTENSIONS--
posix
--SKIPIF--
<?php
-if (!function_exists('posix_setrlimit') || !posix_setrlimit(POSIX_RLIMIT_NOFILE, 2048, -1)) {
+// Keep the hard limit: raising it to unlimited fails even for root on Linux (fs.nr_open)
+$hard = function_exists('posix_getrlimit') ? posix_getrlimit(POSIX_RLIMIT_NOFILE)[1] : -1;
+if (!function_exists('posix_setrlimit') || !posix_setrlimit(POSIX_RLIMIT_NOFILE, 2048, is_int($hard) ? $hard : -1)) {
die('skip Failed to set POSIX_RLIMIT_NOFILE');
}
if (PHP_OS_FAMILY === 'Solaris' && PHP_INT_SIZE === 8)
@@ -13,7 +17,8 @@
--FILE--
<?php
-posix_setrlimit(POSIX_RLIMIT_NOFILE, 2048, -1);
+$hard = posix_getrlimit(POSIX_RLIMIT_NOFILE)[1];
+posix_setrlimit(POSIX_RLIMIT_NOFILE, 2048, is_int($hard) ? $hard : -1);
$fds = [];
for ($i = 0; $i < 1023; $i++) {
@@ -21,21 +26,19 @@
}
list($a, $b) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
+fwrite($b, "x");
set_error_handler(function($errno, $errstr) { throw new \Exception($errstr); });
$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--
-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--
<?php
for ($i = 0; $i < 1023; $i++) {
diff --git a/ext/standard/tests/streams/gh9590-003.phpt b/ext/standard/tests/streams/gh9590-003.phpt
new file mode 100644
index 00000000000..504f0146707
--- /dev/null
+++ b/ext/standard/tests/streams/gh9590-003.phpt
@@ -0,0 +1,70 @@
+--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--
+<?php
+// Keep the hard limit: raising it to unlimited fails even for root on Linux (fs.nr_open)
+$hard = function_exists('posix_getrlimit') ? posix_getrlimit(POSIX_RLIMIT_NOFILE)[1] : -1;
+if (!function_exists('posix_setrlimit') || !posix_setrlimit(POSIX_RLIMIT_NOFILE, 2048, is_int($hard) ? $hard : -1)) {
+ die('skip Failed to set POSIX_RLIMIT_NOFILE');
+}
+if (PHP_OS_FAMILY === 'Solaris' && PHP_INT_SIZE === 8)
+ die('skip Solaris LP64 FD_SETSIZE=65536 not practically exceedable here');
+?>
+--FILE--
+<?php
+
+$hard = posix_getrlimit(POSIX_RLIMIT_NOFILE)[1];
+posix_setrlimit(POSIX_RLIMIT_NOFILE, 2048, is_int($hard) ? $hard : -1);
+
+// A low descriptor, opened before the filler.
+list($lo, $lo_peer) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
+
+$fds = [];
+for ($i = 0; $i < 1023; $i++) {
+ $fds[] = @fopen(__DIR__ . "/GH-9590-003-tmpfile.$i", 'w');
+}
+
+// A descriptor beyond FD_SETSIZE.
+list($hi, $hi_peer) = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
+
+fwrite($lo_peer, "y");
+fwrite($hi_peer, "x");
+
+// $hi is in all three sets and $lo in two, so the read, write and except sets
+// each have to grow past FD_SETSIZE. select() returns the number of bits set
+// across all sets: $lo and $hi readable (2) + $hi writable (1) = 3.
+$r = ['lo' => $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--
+<?php
+for ($i = 0; $i < 1023; $i++) {
+ @unlink(__DIR__ . "/GH-9590-003-tmpfile.$i");
+}
+?>
diff --git a/ext/standard/tests/streams/stream_select_win32_duplicate.phpt b/ext/standard/tests/streams/stream_select_win32_duplicate.phpt
new file mode 100644
index 00000000000..6ecee886f58
--- /dev/null
+++ b/ext/standard/tests/streams/stream_select_win32_duplicate.phpt
@@ -0,0 +1,26 @@
+--TEST--
+stream_select(): a socket listed more than once is counted once on Windows
+--SKIPIF--
+<?php
+if (PHP_OS_FAMILY !== 'Windows') die('skip Windows only');
+?>
+--FILE--
+<?php
+/* On Windows the fd_set is an array of SOCKETs, so a socket listed more than
+ * once in a set used to be stored - and counted by select() - that many times,
+ * unlike the POSIX bitset where setting the same descriptor twice is a no-op.
+ * stream_select() must de-duplicate, so the ready count matches POSIX. */
+[$a, $b] = stream_socket_pair(STREAM_PF_INET, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
+fwrite($b, "x"); // make $a readable
+
+$r = [$a, $a, $a]; // the same socket three times
+$w = $e = null;
+var_dump(stream_select($r, $w, $e, 2, 0)); // ready count must be 1, not 3
+var_dump(count($r)); // but all three entries are kept, as on POSIX
+
+fclose($a);
+fclose($b);
+?>
+--EXPECT--
+int(1)
+int(3)
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 00000000000..1a37ed7977e
--- /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--
+<?php
+if (PHP_OS_FAMILY !== 'Windows') die('skip Windows only');
+?>
+--FILE--
+<?php
+/* On Windows the winsock fd_set is a packed array of SOCKETs capped at the
+ * compile-time FD_SETSIZE (default 256, see --enable-fd-setsize). stream_select()
+ * now grows the set on demand, so it can watch far more than FD_SETSIZE sockets,
+ * matching POSIX behaviour. Here we select on FD_SETSIZE + 50 readable sockets
+ * and expect every one to be reported ready; before the fix the winsock FD_SET
+ * macro silently dropped everything past FD_SETSIZE. */
+$target = PHP_FD_SETSIZE + 50;
+$pairs = [];
+$reads = [];
+for ($i = 0; $i < $target; $i++) {
+ $p = stream_socket_pair(STREAM_PF_INET, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
+ if ($p === false) {
+ break;
+ }
+ fwrite($p[0], "x");
+ $pairs[] = $p;
+ $reads[] = $p[1];
+}
+
+$r = $reads;
+$w = $e = null;
+$ready = stream_select($r, $w, $e, 2, 0);
+
+var_dump(count($reads) === $target);
+var_dump($ready === $target);
+var_dump(count($r) === $target);
+
+foreach ($pairs as $p) {
+ fclose($p[0]);
+ fclose($p[1]);
+}
+?>
+--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 00000000000..5d0ee495d2f
--- /dev/null
+++ b/ext/standard/tests/streams/stream_select_win32_max_handles.phpt
@@ -0,0 +1,38 @@
+--TEST--
+stream_select(): >64 non-socket handles fail gracefully on Windows (no stack overflow)
+--SKIPIF--
+<?php
+if (PHP_OS_FAMILY !== 'Windows') die('skip Windows only');
+?>
+--FILE--
+<?php
+/* On Windows stream_select() multiplexes non-socket handles (pipes, files)
+ * 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. It must instead fail gracefully
+ * (warning + false). */
+$files = [];
+for ($i = 0; $i < 70; $i++) {
+ $path = __DIR__ . "/stream_select_win32_max_handles_$i.tmp";
+ file_put_contents($path, "d");
+ $files[] = fopen($path, "rb");
+}
+
+$r = $files;
+$w = $e = null;
+var_dump(stream_select($r, $w, $e, 0, 0));
+
+foreach ($files as $f) {
+ fclose($f);
+}
+?>
+--EXPECTF--
+Warning: stream_select(): Unable to select [%d]: %s (max_fd=%d) in %s on line %d
+bool(false)
+--CLEAN--
+<?php
+for ($i = 0; $i < 70; $i++) {
+ @unlink(__DIR__ . "/stream_select_win32_max_handles_$i.tmp");
+}
+?>
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 00000000000..1f1e60a3850
--- /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--
+<?php
+if (PHP_OS_FAMILY !== 'Windows') die('skip Windows only');
+?>
+--FILE--
+<?php
+/* The read, write and except sets each grow on demand, independently. Put more
+ * than FD_SETSIZE sockets in all three at once: every socket is readable (data
+ * pending) and writable, none has an exceptional condition. select() returns the
+ * number of set entries across all three sets, so each socket counts twice. */
+$target = PHP_FD_SETSIZE + 50;
+$pairs = [];
+$socks = [];
+for ($i = 0; $i < $target; $i++) {
+ $p = stream_socket_pair(STREAM_PF_INET, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
+ if ($p === false) {
+ break;
+ }
+ fwrite($p[0], "x");
+ $pairs[] = $p;
+ $socks[] = $p[1];
+}
+
+$r = $socks;
+$w = $socks;
+$e = $socks;
+$ready = stream_select($r, $w, $e, 2, 0);
+
+var_dump(count($socks) === $target);
+var_dump($ready === 2 * $target);
+var_dump(count($r) === $target, count($w) === $target, count($e));
+
+/* A write set alone beyond FD_SETSIZE. */
+$r = $e = null;
+$w = $socks;
+var_dump(stream_select($r, $w, $e, 2, 0) === $target);
+
+foreach ($pairs as $p) {
+ fclose($p[0]);
+ fclose($p[1]);
+}
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+int(0)
+bool(true)
diff --git a/main/php_network.h b/main/php_network.h
index 5c2cb0acede..6904f760395 100644
--- a/main/php_network.h
+++ b/main/php_network.h
@@ -248,6 +248,166 @@ 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
+/* A growable fd_set for stream_select(), without the FD_SETSIZE limit.
+ *
+ * On POSIX, fd_set is a bitset indexed by descriptor, and select() takes any nfds: the set
+ * is a bitset that grows to hold the highest descriptor added.
+ * On Windows, fd_set is an array of SOCKETs ({ u_int fd_count; SOCKET fd_array[]; }), and
+ * select() reads fd_count of them: the set is that array, grown to hold every socket added.
+ * Either way, `set` can be passed to select() as it is. */
+#ifdef PHP_WIN32
+typedef struct {
+ u_int capacity; /* SOCKET slots in set->fd_array */
+ fd_set *set;
+} 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)
+{
+ 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);
+ }
+}
+
+static zend_always_inline void php_growable_fd_set_add(php_growable_fd_set *s, php_socket_t fd)
+{
+ /* Appends without the duplicate scan of winsock's FD_SET(); the set is
+ * de-duplicated in one pass later by php_growable_fd_set_sort_unique(). */
+ php_growable_fd_set_reserve(s, s->set->fd_count + 1);
+ s->set->fd_array[s->set->fd_count++] = fd;
+}
+
+static zend_always_inline int php_growable_fd_set_cmp(const void *a, const void *b)
+{
+ SOCKET x = *(const SOCKET *) a;
+ SOCKET y = *(const SOCKET *) b;
+ return (x > y) - (x < y);
+}
+
+/* Sorts the sockets ascending and drops duplicates. This makes select() count a
+ * socket listed more than once (e.g. stream_select([$a, $a], ...)) a single time
+ * - as the POSIX bitset does - and lets php_growable_fd_set_isset() binary-search
+ * instead of scanning, so a whole stream_select() call is O(n log n), not O(n^2). */
+static zend_always_inline void php_growable_fd_set_sort_unique(php_growable_fd_set *s)
+{
+ u_int n = s->set->fd_count;
+ if (n < 2) {
+ return;
+ }
+ qsort(s->set->fd_array, n, sizeof(SOCKET), php_growable_fd_set_cmp);
+ u_int w = 1;
+ for (u_int r = 1; r < n; r++) {
+ if (s->set->fd_array[r] != s->set->fd_array[w - 1]) {
+ s->set->fd_array[w++] = s->set->fd_array[r];
+ }
+ }
+ s->set->fd_count = w;
+}
+
+/* Binary search; the set must have been sorted by php_growable_fd_set_sort_unique(). */
+static zend_always_inline bool php_growable_fd_set_isset(const php_growable_fd_set *s, php_socket_t fd)
+{
+ SOCKET target = (SOCKET) fd;
+ u_int lo = 0, hi = s->set->fd_count;
+ while (lo < hi) {
+ u_int mid = lo + (hi - lo) / 2;
+ SOCKET v = s->set->fd_array[mid];
+ if (v < target) {
+ lo = mid + 1;
+ } else if (v > target) {
+ hi = mid;
+ } else {
+ return true;
+ }
+ }
+ return false;
+}
+
+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) {
+ pefree(s->set, 1);
+ s->set = NULL;
+ }
+ s->capacity = 0;
+}
+#else
+typedef struct {
+ size_t size; /* bytes in set */
+ fd_set *set;
+} php_growable_fd_set;
+
+/* Use the platform's fd_mask: on big-endian systems the bit positions depend on the word size */
+# ifdef NFDBITS
+typedef fd_mask php_growable_fd_mask;
+# define PHP_GROWABLE_FD_SET_WORD_BITS NFDBITS
+# else
+typedef unsigned long php_growable_fd_mask;
+# define PHP_GROWABLE_FD_SET_WORD_BITS (CHAR_BIT * sizeof(unsigned long))
+# endif
+/* select() operates on whole words so the size must be a multiple of sizeof(php_growable_fd_mask) */
+# 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->size = PHP_GROWABLE_FD_SET_SIZE(capacity);
+ s->set = (fd_set *) ecalloc(1, s->size);
+}
+
+/* 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)
+{
+ 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, php_socket_t fd)
+{
+ php_growable_fd_set_reserve(s, fd + 1);
+ ((php_growable_fd_mask *) s->set)[fd / PHP_GROWABLE_FD_SET_WORD_BITS] |= (php_growable_fd_mask) (1UL << (fd % PHP_GROWABLE_FD_SET_WORD_BITS));
+}
+
+static zend_always_inline bool php_growable_fd_set_isset(const php_growable_fd_set *s, php_socket_t fd)
+{
+ return (size_t) fd < s->size * CHAR_BIT
+ && (((const php_growable_fd_mask *) 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
+
#define PHP_SOCK_CHUNK_SIZE 8192
diff --git a/win32/select.c b/win32/select.c
index 988f037d7a4..69b452e1f32 100644
--- a/win32/select.c
+++ b/win32/select.c
@@ -16,6 +16,11 @@
#include "php_network.h"
#include "win32/time.h"
+/* Which of the read/write/except sets a non-socket handle belongs to. */
+#define PHP_SELECT_SET_READ 1
+#define PHP_SELECT_SET_WRITE 2
+#define PHP_SELECT_SET_EXCEPT 4
+
/* Win32 select() will only work with sockets, so we roll our own implementation here.
* - If you supply only sockets, this simply passes through to winsock select().
* - If you supply file handles, there is no way to distinguish between
@@ -34,21 +39,20 @@ PHPAPI int php_select(php_socket_t max_fd, fd_set *rfds, fd_set *wfds, fd_set *e
ULONGLONG ms_total, limit;
HANDLE handles[MAXIMUM_WAIT_OBJECTS];
int handle_slot_to_fd[MAXIMUM_WAIT_OBJECTS];
+ int handle_dirs[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;
- int sock_max_fd = -1;
+ php_growable_fd_set sock_read = {0}, sock_write = {0}, sock_except = {0};
+ php_growable_fd_set aread = {0}, awrite = {0}, aexcept = {0};
+ bool has_sockets = false;
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) {
return -1;
}
-#define SAFE_FD_ISSET(fd, set) (set != NULL && FD_ISSET(fd, set))
-
/* calculate how long we need to wait in milliseconds */
if (tv == NULL) {
ms_total = INFINITE;
@@ -57,70 +61,141 @@ 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++) {
- if (SAFE_FD_ISSET(i, rfds) || SAFE_FD_ISSET(i, wfds) || SAFE_FD_ISSET(i, efds)) {
- int _type;
- int _len = sizeof(_type);
+ /* Split each input set into sockets (driven by winsock select()) and
+ * non-socket handles (pipes/files, waited on with WaitForMultipleObjects()).
+ * A single linear pass over the three fd_arrays; max_fd is not an iteration
+ * bound, as a socket handle value can be very large. */
+ {
+ fd_set *in_sets[3];
+ php_growable_fd_set *sock_sets[3];
+ int set_dirs[3];
- 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);
- }
- if (SAFE_FD_ISSET(i, wfds)) {
- FD_SET((uint32_t)i, &sock_write);
- }
- if (SAFE_FD_ISSET(i, efds)) {
- FD_SET((uint32_t)i, &sock_except);
- }
- 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) {
- num_read_pipes++;
+ in_sets[0] = rfds; sock_sets[0] = &sock_read; set_dirs[0] = PHP_SELECT_SET_READ;
+ in_sets[1] = wfds; sock_sets[1] = &sock_write; set_dirs[1] = PHP_SELECT_SET_WRITE;
+ in_sets[2] = efds; sock_sets[2] = &sock_except; set_dirs[2] = PHP_SELECT_SET_EXCEPT;
+
+ for (int s = 0; s < 3; s++) {
+ fd_set *set = in_sets[s];
+
+ if (set == NULL) {
+ continue;
+ }
+ for (u_int k = 0; k < set->fd_count; k++) {
+ php_socket_t fd = set->fd_array[k];
+ int _type;
+ int _len = sizeof(_type);
+
+ if (getsockopt((SOCKET)fd, SOL_SOCKET, SO_TYPE, (char*)&_type, &_len) == 0 || WSAGetLastError() != WSAENOTSOCK) {
+ /* socket */
+ php_growable_fd_set_add(sock_sets[s], fd);
+ has_sockets = true;
+ } else {
+ HANDLE handle = (HANDLE)(uintptr_t)_get_osfhandle((int)fd);
+ int j;
+
+ if (handle == INVALID_HANDLE_VALUE) {
+ continue;
}
- handle_slot_to_fd[n_handles] = i;
+ /* Dedupe the handle across the read/write/except sets; at most
+ * MAXIMUM_WAIT_OBJECTS entries, so this scan stays short. */
+ for (j = 0; j < n_handles; j++) {
+ if (handle_slot_to_fd[j] == (int)fd) {
+ handle_dirs[j] |= set_dirs[s];
+ break;
+ }
+ }
+ if (j < n_handles) {
+ continue;
+ }
+ 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;
+ handle_slot_to_fd[n_handles] = (int)fd;
+ handle_dirs[n_handles] = set_dirs[s];
n_handles++;
}
}
}
}
+ /* How many of the handles are pipes in the read set (for the data-available
+ * peek further down). */
+ for (i = 0; i < n_handles; i++) {
+ if ((handle_dirs[i] & PHP_SELECT_SET_READ) && GetFileType(handles[i]) == FILE_TYPE_PIPE) {
+ num_read_pipes++;
+ }
+ }
+
+ /* De-duplicate each socket set (a socket may be listed more than once) and
+ * leave it sorted - for select() and for the binary-search isset() back in
+ * stream_array_from_fd_set(). */
+ php_growable_fd_set_sort_unique(&sock_read);
+ php_growable_fd_set_sort_unique(&sock_write);
+ php_growable_fd_set_sort_unique(&sock_except);
+
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. The sets are
+ * growable, so this is no longer bounded by FD_SETSIZE. Pass NULL where
+ * the caller's set was NULL, so the call matches the pre-split one. */
+ retcode = select(-1, rfds ? sock_read.set : NULL, wfds ? sock_write.set : NULL, efds ? sock_except.set : NULL, tv);
+ if (retcode >= 0) {
+ /* select() rewrote the arrays with the ready sockets, unordered;
+ * re-sort so the caller can binary-search, then copy back. */
+ php_growable_fd_set_sort_unique(&sock_read);
+ php_growable_fd_set_sort_unique(&sock_write);
+ php_growable_fd_set_sort_unique(&sock_except);
+ if (rfds) {
+ memcpy(rfds, sock_read.set, PHP_GROWABLE_FD_SET_ALLOC_SIZE(sock_read.set->fd_count));
+ }
+ if (wfds) {
+ memcpy(wfds, sock_write.set, PHP_GROWABLE_FD_SET_ALLOC_SIZE(sock_write.set->fd_count));
+ }
+ if (efds) {
+ memcpy(efds, sock_except.set, PHP_GROWABLE_FD_SET_ALLOC_SIZE(sock_except.set->fd_count));
+ }
+ }
+ 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;
+ if (has_sockets) {
+ /* 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 */
@@ -144,42 +219,59 @@ PHPAPI int php_select(php_socket_t max_fd, fd_set *rfds, fd_set *wfds, fd_set *e
}
for (i = 0; i < n_handles; i++) {
if (WAIT_OBJECT_0 == WaitForSingleObject(handles[i], 0)) {
- if (SAFE_FD_ISSET(handle_slot_to_fd[i], rfds)) {
+ if (handle_dirs[i] & PHP_SELECT_SET_READ) {
DWORD avail_read = 0;
if (num_read_pipes < n_handles
|| !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);
+ if (handle_dirs[i] & PHP_SELECT_SET_WRITE) {
+ 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);
+ if (handle_dirs[i] & PHP_SELECT_SET_EXCEPT) {
+ php_growable_fd_set_add(&aexcept, (SOCKET)(uintptr_t)handle_slot_to_fd[i]);
retcode++;
}
}
}
}
}
- if (retcode == 0 && num_read_pipes == n_handles && sock_max_fd < 0) {
+ if (retcode == 0 && num_read_pipes == n_handles && !has_sockets) {
usleep(100);
}
} while (retcode == 0 && (ms_total == INFINITE || GetTickCount64() < limit));
+ /* Copy the results back into the caller's sets, sorted so the caller can
+ * binary-search them. 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. */
+ php_growable_fd_set_sort_unique(&aread);
+ php_growable_fd_set_sort_unique(&awrite);
+ php_growable_fd_set_sort_unique(&aexcept);
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;
}