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
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
25 changes: 25 additions & 0 deletions ext/standard/tests/streams/gh24173.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
--TEST--
GH-24173 (Sockets from synchronous stream_socket_client() are left in non-blocking mode on Windows)
--EXTENSIONS--
sockets
--SKIPIF--
<?php
if (PHP_OS_FAMILY !== 'Windows') die('skip Windows only');
?>
--FILE--
<?php
$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);

$sock = socket_import_stream($client);
socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 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)
31 changes: 31 additions & 0 deletions ext/standard/tests/streams/gh24173_write_timeout.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
GH-24173 (Timed writes on socket streams must not block past the timeout)
--SKIPIF--
<?php
if (PHP_OS_FAMILY !== 'Windows') die('skip Windows only');
?>
--FILE--
<?php
$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 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; $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)
4 changes: 3 additions & 1 deletion main/network.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) \
Expand Down
13 changes: 13 additions & 0 deletions main/streams/xp_socket.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
}
Expand Down
Loading