Skip to content

Fix stream_select() failing unpredictably (and crashing on Windows) with many descriptors - #14452

Open
frodeborli wants to merge 28 commits into
php:masterfrom
frodeborli:stream_select_unlimited
Open

frodeborli wants to merge 28 commits into
php:masterfrom
frodeborli:stream_select_unlimited

Conversation

@frodeborli

@frodeborli frodeborli commented Jun 3, 2024 •

Copy link
Copy Markdown

stream_select() has a correctness bug: once any watched stream's descriptor exceeds FD_SETSIZE, the call emits a warning and returns false — aborting the entire select, including the low-numbered descriptors that would have worked. PHP userland has no control over or visibility into which integer a stream's descriptor gets, so this failure is effectively nondeterministic from the script's perspective: the same code works or fails depending on incidental descriptor numbering. The only escape was recompiling with --enable-fd-setsize.

This fixes the root cause: stream_select() no longer uses a fixed-size fd_set and is no longer bounded by FD_SETSIZE. No recompilation needed.

POSIX: builds a heap bitset sized to the descriptors in use, rounded to the word granularity select() reads/writes.

Windows: the winsock fd_set is a packed SOCKET array bounded by FD_SETSIZE; the socket path now uses a growable fd_set that doubles on demand. php_select()'s exported fd_set * ABI is unchanged.

Also fixes a Windows crash: on the non-socket (pipe/file) path, php_select() pushed each handle into a fixed 64-entry stack array with no bounds check, so more than 64 handles overran it. That path is bounded by WaitForMultipleObjects()'s MAXIMUM_WAIT_OBJECTS (64) limit anyway, so it now returns an error (caller warns and returns false) instead. Fixed inside php_select(), so sapi/cli/php_cli_server.c is covered too.

Scope: stream_select() only. --enable-fd-setsize remains meaningful for socket_select(), mysqlnd, the FPM select backend, and other consumers, which are unchanged.

Tests: ext/standard/tests/streams passes on Linux, FreeBSD, macOS, Windows. gh9590-001/002.phpt updated to assert correct selection past the old limit. Two Windows-only tests added.


This is a bug fix, not a behavior change, and I'd like it considered for all supported branches. The differences an existing script can observe — the return value, the removed E_WARNING, and blocking instead of returning immediately — occur only on calls that previously emitted an E_WARNING and returned false, i.e. calls PHP was already reporting as failures. Any call that was selecting successfully (all descriptors below FD_SETSIZE) behaves identically. There is no warning-free, working call whose behavior changes. The Windows >64-handle case is an outright crash fix. On that basis I believe it qualifies for backport to the active stable branches rather than master only.

The patch does not apply cleanly to the older branches as-is (stream_select() was refactored on master, and the Windows pipe handling differs across versions), so I'm happy to provide adapted patches per branch if backporting is wanted.

…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
…ow if those functions could be referenced by other extensions.
@frodeborli

Copy link
Copy Markdown
Author

The testing complained that I had left behind two functions which I didn't dare to remove in case somebody else was using them. I have updated the pr.

@frodeborli

Copy link
Copy Markdown
Author

I'm not sure why the tests timed out or failed; it does not seem to be related to my patch.

@arnaud-lb arnaud-lb left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big +1 on the general idea. This fixes a long standing issue with stream_select() not supporting file descriptors above 1024.

@bukka @devnexen wdyt?

Maybe we can remove the --enable-fd-setsize configure flag as well. I believe it's a no-op in glibc for some time now.

Comment thread ext/standard/streamsfuncs.c Outdated
Comment thread ext/standard/streamsfuncs.c Outdated
Comment thread ext/standard/streamsfuncs.c Outdated
Comment thread ext/standard/streamsfuncs.c
Comment thread ext/standard/streamsfuncs.c Outdated
Comment thread ext/standard/streamsfuncs.c Outdated
return ret;
}
/* }}} */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be tested from useland when more than 1024 file descriptors are created/used?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also when there is less than 1024, but at least one has a number > 1024

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mvorisek I used this code to test:

<?php
// Create a large number of file descriptors
$files = [];
$r = $w = $e = [];

for ($i = 0; $i < 20000; $i++) {
    $fileName = tempnam(sys_get_temp_dir(), 'testfile');
    $fp = fopen($fileName, 'r');
    if ($fp === false) {
        die("Failed to open file: $fileName\n");
    }
    $files[] = ['name' => $fileName, 'handle' => $fp];
    $r[] = $fp;
}

// Check the number of file descriptors
$res = stream_select($r, $w, $e, 0, 0);
var_dump($res, $r);
// Clean up
foreach ($files as $file) {
    fclose($file['handle']);
    unlink($file['name']);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A function doing dup2() can probably be added to zend_test, allowing to zend_test_dup2(1, 9999).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TimWolla Is zend_test a mechanism to add special functions to the PHP runtime during testing? I think it would be nice to have zend_test_get_fd($resource), but perhaps that already exists?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@frodeborli

Is zend_test a mechanism to add special functions to the PHP runtime during testing?

Yes, exactly. It is meant to provide helpers to test stuff that would otherwise be untestable. You can find it in https://github.com/php/php-src/tree/master/ext/zend_test and can enable it at build time using --enable-zend-test.

@frodeborli

Copy link
Copy Markdown
Author

I will look over the PR thoroughly later today or tomorrow morning.

Can anybody tell me how to build this in a stricter mode? Several commits could have been avoided by me if only my environment would let me know about unused functions etc, instead of waiting for the github CI process to check for me.

@devnexen

devnexen commented Jun 3, 2024

Copy link
Copy Markdown
Member

I will look over the PR thoroughly later today or tomorrow morning.

Can anybody tell me how to build this in a stricter mode? Several commits could have been avoided by me if only my environment would let me know about unused functions etc, instead of waiting for the github CI process to check for me.

Speaking of CI, you can get inspiration from it if you like.

Comment thread ext/standard/streamsfuncs.c Outdated
Comment thread ext/standard/streamsfuncs.c Outdated
Comment thread ext/standard/streamsfuncs.c
Comment thread ext/standard/streamsfuncs.c Outdated
@ramsey

ramsey commented Jun 4, 2024

Copy link
Copy Markdown
Member

The only feedback I would give is to please update your editor to respect the .editorconfig file provided with PHP. https://editorconfig.org

It looks like you've converted a lot of tab characters to spaces, so the diff is much bigger than it should be.

Comment thread ext/standard/streamsfuncs.c Outdated
@frodeborli

frodeborli commented Jun 4, 2024 •

Copy link
Copy Markdown
Author

I am having trouble because stream wrappers emit warnings without respecting the silent cast argument of php_stream_cast...

So a phpt test does not generate the exact same output anymore after my patch.

@arnaud-lb

arnaud-lb commented Jun 4, 2024 •

Copy link
Copy Markdown
Member

The cause of the extra warnings in ext/standard/tests/file/userstreams_002.phpt appear to be that max_fd is never equal to -1 here.

Comment thread ext/standard/streamsfuncs.c Outdated
Comment thread ext/standard/streamsfuncs.c
@frodeborli

Copy link
Copy Markdown
Author

Now it seems all tests succeeded, except Windows 64 bit. Not sure how to proceed. I think the php_select macro refers to a win32 implementation which is meant to be compatible with select() in Linux and therefore should work.

Comment thread ext/standard/streamsfuncs.c Outdated
}

if (!PHP_SAFE_MAX_FD(max_fd, max_set_count)) {
if (max_set_count == 0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we missing FD_BIGSET_FREE before returning?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure where you mean; I think FD_BIGSET_FREE is everywhere it is needed now, but there was a few places they were missing before the last push.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've already called FD_BIGSET_ZERO by this point, so the memory is allocated. On this line, we are about to return without freeing that memory; thus I think we need to free it.

That being said, I'm not quite sure how we would get here.

Comment thread ext/standard/streamsfuncs.c Outdated
@frodeborli

frodeborli commented Jun 7, 2024 via email

Copy link
Copy Markdown
Author

Comment thread ext/standard/streamsfuncs.c Outdated
@frodeborli

frodeborli commented Jun 27, 2024 via email •

Copy link
Copy Markdown
Author

Comment thread ext/standard/streamsfuncs.c Outdated
Comment thread ext/standard/streamsfuncs.c Outdated
Comment thread ext/standard/streamsfuncs.c Outdated
Comment thread ext/standard/streamsfuncs.c Outdated
Comment thread ext/standard/streamsfuncs.c Outdated
Comment thread ext/standard/streamsfuncs.c Outdated
Comment thread ext/standard/streamsfuncs.c Outdated
Comment thread ext/standard/streamsfuncs.c Outdated
Comment on lines +940 to +942
size_t largest = rfds.size;
if (largest < wfds.size) largest = wfds.size;
if (largest < efds.size) largest = efds.size;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can use max_fd to directly compute the largest size?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like:

while (max_fd > rfds.size) fd_bigset_double_size(&rfds);

I was just not confident about possibly off by one error here...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use the same condition as in FD_BIGSET_ENSURE_CAPACITY(), or pre-compute the largest size with:

size_t num_fds = max_fd + 1;
size_t largest = (num_fds + 7) / 8;

Comment thread ext/standard/streamsfuncs.c Outdated
@frodeborli

Copy link
Copy Markdown
Author

@arnaud-lb Thank you for your detailed review. I'm merging the latest php-src and pushing an updated PR with all your suggestions.

I was looking into Windows which apparently only has FD_SETSIZE 64 by default, and it uses an entirely different fd_set structure. Not sure how this is handled inside PHP and if my approach breaks it? I've not got a Windows C development environment, but I can look into it if you can give some hints perhaps?

@arnaud-lb

Copy link
Copy Markdown
Member

Oh right, it looks like this will break on Windows. We use WinSock select() according to this comment, so fd_set is as defined here: https://learn.microsoft.com/en-us/windows/win32/api/winsock/ns-winsock-fd_set on Windows. It represents an array of handles rather than a bitset, and the limit is the number of watched fds rather than the max fd value.

We can use the same strategy as for other platforms, but use the fd count to decide whether to grow the set. We have to define the FD_BIGSET_ macros differently on windows:

#ifdef PHP_WIN32

typedef struct {
    uint32_t capacity;
    fd_set set;
} fd_bigset;

#define FD_BIGSET_ENSURE_CAPACITY(fd, set) \
    if (UNEXPECTED(fd.set.count == fd.capacity)) { \
        fd_bigset_double_size(set); \
    }

// other FD_BIGSET macros for windows

#else 

// FD_BIGSET macros for non-windows

#endif

However there is a (non-enforced) hard limit of MAXIMUM_WAIT_OBJECTS (64) non-socket fds in win32/select.c. I would suggest to return a failure from php_select() when there are more than MAXIMUM_WAIT_OBJECTS non-socket fds.

php_select() copies fd_set structs by assignment (fd_set sock_read, aread; ...; aread = sock_read;). This will not work with custom sized sets, so we need to replace these with memcpy()s.

It would make sense to do part of this work later in a separate PR, as long as this PR doesn't break windows. E.g. don't support more than FD_SETSIZE fds for now on Windows.

I've not got a Windows C development environment, but I can look into it if you can give some hints perhaps?

I sometimes use a Windows VM from https://developer.microsoft.com/en-us/windows/downloads/virtual-machines/ (The VirtualBox ones work well. If you use virt-manager you may be able to convert them to KVM but that's not trivial.). This page describes how to build on windows: https://wiki.php.net/internals/windows/stepbystepbuild_sdk_2

@bukka bukka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will take a proper look later but could you please in the meantime fix the coding style.

Comment thread ext/standard/streamsfuncs.c Outdated
@bukka

bukka commented Jul 5, 2024

Copy link
Copy Markdown
Member

btw I have a real Windows env so I can test it then too.

frodeborli and others added 5 commits September 22, 2026 10:43
…onfig

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 <noreply@anthropic.com>
Reconciles with ~1.5 years of upstream development, including the
mechanical stream_select() refactor in b46c5e6 (const HashTable*
params, dropped dead IS_ARRAY checks) and the new optional $context
parameter / stream error API additions. The only real conflict was in
ext/standard/streamsfuncs.c, resolved by applying the same
modernization to our fd_bigset (POSIX) and native fd_set (Windows)
code paths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…g over it

These tests (arnaud-lb, phpGH-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 phpGH-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 <noreply@anthropic.com>
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.
…ording

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.
@frodeborli frodeborli changed the title Updated the stream_select() function so that it supports bigger file … Fix stream_select() failing unpredictably (and crashing on Windows) with many descriptors Sep 22, 2026
@frodeborli
frodeborli requested a review from bukka September 22, 2026 13:46
@frodeborli

Copy link
Copy Markdown
Author

The WINDOWS_X64_ZTS failure here is unrelated to this change. The build succeeds (vs18, /WX), and the only failing test out of ~23.7k is ext/standard/tests/file/GHSA-9f67-6fw4-hpfp-win32.phpt, which fails during its own cleanup (Warning: rmdir(): Directory not empty). That test is currently failing on master too — e.g. the Windows job in run 35732538919 — so it's a pre-existing issue independent of this PR. The stream_select() changes compile cleanly and the new stream_select_win32_* tests pass.

frodeborli added a commit to phasync/phasync that referenced this pull request Sep 22, 2026
…rocess/Windows simplification

- phasync-ext (github.com/phasync/phasync-ext, MIT, PHP 8.3+) exists and is tested: a real
  Zend extension delivering a growable stream_select() with no FD_SETSIZE ceiling, plus
  transparent async hooks for tcp/unix/ssl sockets, proc_open() pipes, and the sleep()
  family. Verified locally: all 7 of its phpt tests pass against the built module. This
  supersedes the FFI-based plan for directions A and B; the FFI research is kept as the
  record of the reasoning, not deleted.
- php/php-src#14452 (stream_select_unlimited): live status folded in from the coordinating
  session's own report -- code-complete, Windows verified on real hardware and CI, the one
  red CI job diagnosed as an unrelated pre-existing master-wide flake, still awaiting
  maintainer review.
- GH-16889 (Windows pipe polling, shipped in PHP 8.5) and the io/poll RFC and Scheduler ABI
  pre-RFC researched and folded in.
- Process/Windows: dropped the planned custom fork()/exec()/pipe() process launcher --
  PosixProcessRunner already just uses proc_open() + stream_select(), so this is now a
  version-floor decision, not an engineering project, once the above land.
- New sections: the clustering/string-broadcast primitive and how it resolves D2, and a
  checklist for narrowing the core (Psr/*, FastCGI, the PDO wrapper, fork()).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEVK2ZKB8msUGY6D5gp9F9
@frodeborli

Copy link
Copy Markdown
Author

Excited to pick this back up — apologies for the long silence since last year, some personal circumstances kept me away from this for a while. Good to be back on it.

Rebased and pushed 2 days ago (test run 35733334346) — CI is green across Linux (x64/x32, ASAN/DEBUG), Alpine, macOS ARM64, FreeBSD, and CircleCI arm. The one red job, WINDOWS_X64_ZTS, is ghsa-9f67-6fw4-hpfp-win32.phpt failing during its own cleanup (rmdir(): Directory not empty) — unrelated to this change and also currently red on master itself (e.g. run 35732538919). The stream_select_win32_* tests this PR adds pass.

Re the ABI break label: I believe this is the path-based labeler matching main/*.h (this PR touches main/php_network.h), not a reflection of an actual break — the diff to that file is a pure addition (php_growable_fd_set and its static-inline helpers), nothing existing is resized, relocated, or given a different signature. php_select()'s exported fd_set * signature is unchanged, as noted in the PR description. Happy to be corrected if I'm missing something.

@bukka — thanks for taking another look. @arnaud-lb — tagging you too since you did most of the substantive review back in 2024 (the FD_BIGSET design, the Windows fd_set-as-array constraints) and the Windows growable-fd_set approach here follows the shape you suggested. Would appreciate your eyes on the final form if you have time.

@arnaud-lb

Copy link
Copy Markdown
Member

@frodeborli Is this still worth the effort now that we have https://wiki.php.net/rfc/poll_api to replace stream_select()?

@frodeborli

frodeborli commented Sep 25, 2026 •

Copy link
Copy Markdown
Author

Thanks @arnaud-lb. I checked master: the poll API added Io\Poll as a separate API and left stream_select() untouched — it's still the select()-based code with fixed-size fd_set rfds, wfds, efds on the stack. So this isn't superseded; the bug is still live in master.

And it is a real bug, not just a limit. Past FD_SETSIZE, FD_SET() writes past those stack fd_sets — stack corruption, i.e. a crash (and worse than a clean failure). Because fd numbers grow with the number of open connections, a server that accepts enough of them can be pushed over the edge, so this is a plausible DoS in any app using stream_select() at scale. Io\Poll existing doesn't help any of that already-written code — including the default pure-PHP event loops in ReactPHP, Revolt/amphp and phasync, which fall back to stream_select() when ext-event/ev/uv aren't installed (the common case).

I'd also argue the two are complementary rather than redundant. Io\Poll (epoll/kqueue) wins for large numbers of long-lived connections; select()/poll() semantics rebuild the set per call with no per-fd registration syscall, which is cheaper under high connection churn (typical request/response servers). Keeping stream_select() correct keeps that regime viable.

The fix is implemented and green across platforms, and the Windows growable fd_set follows the design you outlined in 2024. Personally, I really want this feature in PHP, even though Io\Poll is coming.

frodeborli and others added 3 commits September 25, 2026 12:28
The existing phpGH-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.
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).
@frodeborli

Copy link
Copy Markdown
Author

@bukka The tabs-vs-spaces issue from your review is fixed; the diff now uses tabs throughout. I've also added tests for descriptors beyond FD_SETSIZE in the write and except sets, on both POSIX (gh9590-003) and Windows (stream_select_win32_multi_set, verified on the Windows CI build). Until now only the read set was covered.

Since this is a bug fix (stack corruption past FD_SETSIZE), I think it's still needed alongside Io\Poll, and it doesn't conflict with it. I looked at implementing stream_select() on top of epoll instead, but it's a poor fit: stream_select() is stateless, since every call passes fresh arrays. So each call would need epoll_create1(), one epoll_ctl() per descriptor, epoll_wait() and close(). That's n+2 syscalls where select() needs one. poll() changes the semantics in small ways: select() counts a descriptor once per set it's ready in while poll() counts it once, an invalid fd becomes POLLNVAL instead of EBADF, and timeouts drop from microseconds to milliseconds. I ran into exactly those differences in a poll()-based stream_select() in an extension. Keeping select() with a growable fd_set is the smallest change that fixes the bug while keeping stream_select()'s behaviour identical. Io\Poll remains the right tool for new code.

Could you take another look when you have time?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants