Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,15 @@ PHP NEWS
objects). (Labish Bardiya)
. Fix use-after-free in Randomizer::shuffleArray() when the engine throws.
(Marti2203)
. 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)

- Streams:
. Fixed bug GH-24508 (HTTP wrapper leaks the dechunk filter when header
Expand Down
68 changes: 33 additions & 35 deletions ext/standard/streamsfuncs.c
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ PHP_FUNCTION(stream_get_wrappers)
/* }}} */

/* {{{ stream_select related functions */
static int stream_array_to_fd_set(zval *stream_array, fd_set *fds, php_socket_t *max_fd)
static int stream_array_to_fd_set(zval *stream_array, php_growable_fd_set *fds, php_socket_t *max_fd)
{
zval *elem;
php_stream *stream;
Expand Down Expand Up @@ -652,7 +652,7 @@ static int stream_array_to_fd_set(zval *stream_array, fd_set *fds, php_socket_t
* */
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;
Expand All @@ -663,7 +663,7 @@ static int stream_array_to_fd_set(zval *stream_array, fd_set *fds, php_socket_t
return cnt ? 1 : 0;
}

static int stream_array_from_fd_set(zval *stream_array, 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;
Expand Down Expand Up @@ -691,7 +691,7 @@ static int stream_array_from_fd_set(zval *stream_array, 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)) {
if (!key) {
dest_elem = zend_hash_index_update(ht, num_ind, elem);
} else {
Expand Down Expand Up @@ -767,13 +767,12 @@ PHP_FUNCTION(stream_select)
{
zval *r_array, *w_array, *e_array;
struct timeval tv, *tv_p = NULL;
fd_set rfds, wfds, efds;
php_growable_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)
Expand All @@ -784,55 +783,41 @@ PHP_FUNCTION(stream_select)
Z_PARAM_LONG_OR_NULL(usec, usecnull)
ZEND_PARSE_PARAMETERS_END();

FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_ZERO(&efds);
/* The sets grow as streams are added, past FD_SETSIZE */
php_growable_fd_set_init(&rfds, FD_SETSIZE);
php_growable_fd_set_init(&wfds, FD_SETSIZE);
php_growable_fd_set_init(&efds, FD_SETSIZE);

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;
sets += stream_array_to_fd_set(r_array, &rfds, &max_fd);
}

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;
sets += stream_array_to_fd_set(w_array, &wfds, &max_fd);
}

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;
sets += stream_array_to_fd_set(e_array, &efds, &max_fd);
}

if (!sets) {
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) {
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 */
if (!secnull) {
if (sec < 0) {
zend_argument_value_error(4, "must be greater than or equal to 0");
RETURN_THROWS();
goto cleanup;
} else if (usec < 0) {
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 */
Expand All @@ -855,23 +840,36 @@ 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);

if (retval == -1) {
php_error_docref(NULL, E_WARNING, "Unable to select [%d]: %s (max_fd=%d)",
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);
}
/* }}} */

Expand Down
24 changes: 13 additions & 11 deletions ext/standard/tests/streams/gh9590-001.phpt
Original file line number Diff line number Diff line change
@@ -1,37 +1,39 @@
--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');
}
?>
--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++) {
$fds[] = @fopen(__DIR__ . "/GH-9590-001-tmpfile.$i", 'w');
}

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++) {
Expand Down
25 changes: 14 additions & 11 deletions ext/standard/tests/streams/gh9590-002.phpt
Original file line number Diff line number Diff line change
@@ -1,39 +1,42 @@
--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');
}
?>
--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++) {
$fds[] = @fopen(__DIR__ . "/GH-9590-002-tmpfile.$i", 'w');
}

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++) {
Expand Down
70 changes: 70 additions & 0 deletions ext/standard/tests/streams/gh9590-003.phpt
Original file line number Diff line number Diff line change
@@ -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");
}
?>
26 changes: 26 additions & 0 deletions ext/standard/tests/streams/stream_select_win32_duplicate.phpt
Original file line number Diff line number Diff line change
@@ -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)
Loading
Loading