From 9136a2c8b66cd5a1b19a8dc3c27b28053b83d882 Mon Sep 17 00:00:00 2001 From: vibbow Date: Wed, 7 Oct 2026 15:59:23 +0800 Subject: [PATCH 1/4] Fix GH-24173: Sockets left in non-blocking mode on Windows php_network_connect_socket() switches the socket to non-blocking mode for the connect and restores the original mode afterwards for synchronous connects. On Windows, SET_SOCKET_BLOCKING_MODE() sets `save = TRUE` and FIONBIO does not write back the previous mode, so RESTORE_SOCKET_BLOCKING_MODE() set the socket to non-blocking again. Winsock cannot query a socket's blocking mode, but all callers pass freshly created (blocking) sockets, so restore to blocking. This matches the POSIX behavior. Co-Authored-By: Claude Opus 5.5 --- NEWS | 4 ++++ ext/standard/tests/streams/gh24173.phpt | 25 +++++++++++++++++++++++++ main/network.c | 4 +++- 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 ext/standard/tests/streams/gh24173.phpt diff --git a/NEWS b/NEWS index 999d1f6fc5ae..3cc806719d59 100644 --- a/NEWS +++ b/NEWS @@ -18,6 +18,10 @@ PHP NEWS . Fixed chown() and lchown() failing to resolve user names in ZTS builds when getpwnam_r() needs a larger buffer. (Ilia Alshanetsky) +- Streams: + . Fixed bug GH-24173 (Sockets from synchronous stream_socket_client() are + left in non-blocking mode on Windows). (vibbow) + 22 Oct 2026, PHP 8.4.27 - BCMath: diff --git a/ext/standard/tests/streams/gh24173.phpt b/ext/standard/tests/streams/gh24173.phpt new file mode 100644 index 000000000000..feaf5ce46260 --- /dev/null +++ b/ext/standard/tests/streams/gh24173.phpt @@ -0,0 +1,25 @@ +--TEST-- +GH-24173 (Sockets from synchronous stream_socket_client() are left in non-blocking mode on Windows) +--EXTENSIONS-- +sockets +--SKIPIF-- + +--FILE-- + 0, 'usec' => 100000]); + +// No data is sent: a blocking socket waits and times out, while a +// non-blocking one fails at once with WSAEWOULDBLOCK. +var_dump(@socket_read($sock, 10)); +var_dump(socket_last_error($sock) === SOCKET_ETIMEDOUT); +?> +--EXPECT-- +bool(false) +bool(true) diff --git a/main/network.c b/main/network.c index 40a80a6f2abc..e6e4c96d5624 100644 --- a/main/network.c +++ b/main/network.c @@ -286,8 +286,10 @@ PHPAPI int php_network_getaddresses(const char *host, int socktype, struct socka typedef u_long php_non_blocking_flags_t; # define SET_SOCKET_BLOCKING_MODE(sock, save) \ save = TRUE; ioctlsocket(sock, FIONBIO, &save) +/* Winsock cannot query the current mode; callers pass freshly created + * (blocking) sockets, so restore to blocking. */ # define RESTORE_SOCKET_BLOCKING_MODE(sock, save) \ - ioctlsocket(sock, FIONBIO, &save) + save = FALSE; ioctlsocket(sock, FIONBIO, &save) #else typedef int php_non_blocking_flags_t; # define SET_SOCKET_BLOCKING_MODE(sock, save) \ From 48a00ba6a392e558e2716cb0333910b994c77652 Mon Sep 17 00:00:00 2001 From: vibbow Date: Sat, 10 Oct 2026 01:32:36 +0800 Subject: [PATCH 2/4] Make timed socket writes non-blocking on Windows Windows has no MSG_DONTWAIT, so now that the socket is restored to blocking mode after a synchronous connect, send() could block past the stream timeout. Switch to non-blocking mode for the duration of a timed write and restore blocking mode afterwards, as xp_ssl.c does. This also fixes timed writes on accepted sockets and after stream_set_blocking(true), which were already blocking on Windows. Co-Authored-By: Claude Opus 5.5 --- .../tests/streams/gh24173_write_timeout.phpt | 20 +++++++++++++++++++ main/streams/xp_socket.c | 13 ++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 ext/standard/tests/streams/gh24173_write_timeout.phpt diff --git a/ext/standard/tests/streams/gh24173_write_timeout.phpt b/ext/standard/tests/streams/gh24173_write_timeout.phpt new file mode 100644 index 000000000000..428ab77f93d4 --- /dev/null +++ b/ext/standard/tests/streams/gh24173_write_timeout.phpt @@ -0,0 +1,20 @@ +--TEST-- +GH-24173 (Timed writes on socket streams must not block past the timeout) +--FILE-- + $client, 'accepted' => $conn] as $name => $stream) { + stream_set_timeout($stream, 0, 200000); + for ($i = 0; $i < 256 && @fwrite($stream, $chunk); $i++); + echo $name, ': '; + var_dump(stream_get_meta_data($stream)['timed_out']); +} +?> +--EXPECT-- +client: bool(true) +accepted: bool(true) diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index 69eb078323be..aa5fd53ae7e9 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -69,6 +69,13 @@ static ssize_t php_sockop_write(php_stream *stream, const char *buf, size_t coun else ptimeout = &sock->timeout; +#ifdef PHP_WIN32 + /* There is no MSG_DONTWAIT on Windows, so switch to non-blocking mode for a + * timed write; otherwise send() could block past the timeout. */ + bool restore_blocking = sock->is_blocked && ptimeout + && php_set_sock_blocking(sock->socket, 0) == SUCCESS; +#endif + retry: didwrite = send(sock->socket, buf, XP_SOCK_BUF_SIZE(count), (sock->is_blocked && ptimeout) ? MSG_DONTWAIT : 0); @@ -113,6 +120,12 @@ static ssize_t php_sockop_write(php_stream *stream, const char *buf, size_t coun } } +#ifdef PHP_WIN32 + if (restore_blocking) { + php_set_sock_blocking(sock->socket, 1); + } +#endif + if (didwrite > 0) { php_stream_notify_progress_increment(PHP_STREAM_CONTEXT(stream), didwrite, 0); } From 5529795e5d54f8ed2bc7a64ab0dd6f6d29b5d0d4 Mon Sep 17 00:00:00 2001 From: vibbow Date: Sat, 10 Oct 2026 02:29:37 +0800 Subject: [PATCH 3/4] Stop gh24173_write_timeout.phpt at the first timed-out write A write that times out may still have written part of the chunk, so fwrite() returns a non-zero length. On macOS this happened on many iterations, and the test hit the run-tests timeout. Stop at the first write that reports a timeout instead. Co-Authored-By: Claude Opus 5.5 --- ext/standard/tests/streams/gh24173_write_timeout.phpt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ext/standard/tests/streams/gh24173_write_timeout.phpt b/ext/standard/tests/streams/gh24173_write_timeout.phpt index 428ab77f93d4..488be5158c45 100644 --- a/ext/standard/tests/streams/gh24173_write_timeout.phpt +++ b/ext/standard/tests/streams/gh24173_write_timeout.phpt @@ -6,11 +6,18 @@ $srv = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr); $client = stream_socket_client('tcp://' . stream_socket_get_name($srv, false), $errno, $errstr, 5); $conn = stream_socket_accept($srv); -// Neither side reads, so each write eventually fills the socket buffers. +// Neither side reads, so writes eventually fill the socket buffers. A write +// that times out may still have written part of the chunk, so stop at the +// first timeout rather than at the first zero-length write. $chunk = str_repeat('x', 1 << 20); foreach (['client' => $client, 'accepted' => $conn] as $name => $stream) { stream_set_timeout($stream, 0, 200000); - for ($i = 0; $i < 256 && @fwrite($stream, $chunk); $i++); + for ($i = 0; $i < 256; $i++) { + @fwrite($stream, $chunk); + if (stream_get_meta_data($stream)['timed_out']) { + break; + } + } echo $name, ': '; var_dump(stream_get_meta_data($stream)['timed_out']); } From d4b7a431b049564ee95984e27cfcefcac0765299 Mon Sep 17 00:00:00 2001 From: vibbow Date: Sat, 10 Oct 2026 09:22:35 +0800 Subject: [PATCH 4/4] Run gh24173_write_timeout.phpt on Windows only The test still hit the run-tests timeout on macOS, although the fix is Windows only, so timed writes to a stalled loopback peer don't time out reliably there either way. The bug and the fix are Windows specific, so limit the test to Windows like gh24173.phpt. Co-Authored-By: Claude Opus 5.5 --- ext/standard/tests/streams/gh24173_write_timeout.phpt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ext/standard/tests/streams/gh24173_write_timeout.phpt b/ext/standard/tests/streams/gh24173_write_timeout.phpt index 488be5158c45..85babba8db2a 100644 --- a/ext/standard/tests/streams/gh24173_write_timeout.phpt +++ b/ext/standard/tests/streams/gh24173_write_timeout.phpt @@ -1,5 +1,9 @@ --TEST-- GH-24173 (Timed writes on socket streams must not block past the timeout) +--SKIPIF-- + --FILE--