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/ext/standard/tests/streams/gh24173_write_timeout.phpt b/ext/standard/tests/streams/gh24173_write_timeout.phpt new file mode 100644 index 000000000000..85babba8db2a --- /dev/null +++ b/ext/standard/tests/streams/gh24173_write_timeout.phpt @@ -0,0 +1,31 @@ +--TEST-- +GH-24173 (Timed writes on socket streams must not block past the timeout) +--SKIPIF-- + +--FILE-- + $client, 'accepted' => $conn] as $name => $stream) { + stream_set_timeout($stream, 0, 200000); + 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']); +} +?> +--EXPECT-- +client: bool(true) +accepted: 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) \ 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); }