[php-src] Issue #24173: Sockets from synchronous stream_socket_client() are left in non-blocking mode on Windows

From: Date: Wed, 07 Oct 2026 07:56:04 +0000
Subject: [php-src] Issue #24173: Sockets from synchronous stream_socket_client() are left in non-blocking mode on Windows
Groups: php.bugs 
Request: Send a blank email to php-bugs+get-252933@lists.php.net to get a copy of this message
Issue: https://github.com/php/php-src/issues/24173 Author: vibbow ### Description On Windows, a socket created by a synchronous stream_socket_client() / fsockopen() connect stays in non-blocking mode at the OS level, while the stream reports blocked => true. fread()/fwrite() hide this because they poll before calling recv()/send(). Paths that call recv() directly don't, so they fail immediately instead of waiting for data: - stream_socket_recvfrom() returns false at once. - socket_import_stream() marks the imported socket as blocking (it copies the stream's is_blocked), but socket_read()/socket_recv() fail immediately with WSAEWOULDBLOCK (10035). I found this while investigating #24171. It is a separate bug and independent of that fix. The following code: ```php <?php // Child process: a server that replies "hello" to each connection 300 ms // after accepting it. $server = <<<'SRV' $srv = stream_socket_server('tcp://127.0.0.1:0', $en, $es); echo stream_socket_get_name($srv, false), "\n"; $pending = []; while (true) { $r = [$srv]; $w = $e = null; if (stream_select($r, $w, $e, 0, 10000)) { $pending[] = [stream_socket_accept($srv), hrtime(true) + 300e6]; } foreach ($pending as $i => [$c, $due]) { if (hrtime(true) >= $due) { @fwrite($c, "hello"); fclose($c); unset($pending[$i]); } } } SRV; $proc = proc_open([PHP_BINARY, '-n', '-r', $server], [1 => ['pipe', 'w']], $pipes); $addr = trim(fgets($pipes[1])); function test(string $name, string $addr, callable $fn): void { $c = stream_socket_client("tcp://$addr", $en, $es, 5); $blocked = stream_get_meta_data($c)['blocked'] ? 'true' : 'false'; $t = hrtime(true); $r = $fn($c); printf("%-46s blocked=%s %-28s after %5.1f ms\n", $name, $blocked, var_export($r, true), (hrtime(true) - $t) / 1e6); fclose($c); } test('stream_socket_recvfrom()', $addr, fn($c) => stream_socket_recvfrom($c, 100)); if (extension_loaded('sockets')) { test('socket_import_stream() + socket_read()', $addr, function ($c) { $s = socket_import_stream($c); $r = @socket_read($s, 100); return $r === false ? 'false (error ' . socket_last_error($s) . ')' : $r; }); } test('stream_set_blocking(true) + stream_socket_recvfrom()', $addr, function ($c) { stream_set_blocking($c, true); return stream_socket_recvfrom($c, 100); }); proc_terminate($proc); ``` Resulted in this output: ``` stream_socket_recvfrom() blocked=true false after 0.0 ms socket_import_stream() + socket_read() blocked=true 'false (error 10035)' after 0.0 ms stream_set_blocking(true) + stream_socket_recvfrom() blocked=true 'hello' after 293.7 ms ``` But I expected this output instead (this is the output with the fix below applied, and what the POSIX code path does): ``` stream_socket_recvfrom() blocked=true 'hello' after 304.2 ms socket_import_stream() + socket_read() blocked=true 'hello' after 303.6 ms stream_set_blocking(true) + stream_socket_recvfrom() blocked=true 'hello' after 305.2 ms ``` Explicitly calling stream_set_blocking($c, true) (the last line) works around it, because it calls ioctlsocket(FIONBIO, 0). ### Cause php_network_connect_socket() in main/network.c switches the socket to non-blocking mode for the connect. For a synchronous connect, it is supposed to restore the original mode afterwards. The Windows versions of the macros are: ```c #ifdef PHP_WIN32 typedef u_long php_non_blocking_flags_t; # define SET_SOCKET_BLOCKING_MODE(sock, save) \ save = TRUE; ioctlsocket(sock, FIONBIO, &save) # define RESTORE_SOCKET_BLOCKING_MODE(sock, save) \ ioctlsocket(sock, FIONBIO, &save) ``` SET sets save = TRUE, and FIONBIO only reads its argument; it does not write back the previous mode. So RESTORE passes TRUE again, and the socket stays non-blocking. The POSIX versions save the flags with fcntl(F_GETFL) and restore them correctly. ### Proposed fix Winsock has no way to query a socket's current blocking mode. However, every caller of php_network_connect_socket() passes a freshly created socket, which is blocking by default: - php_network_connect_socket_to_host() - the Unix socket connect in main/streams/xp_socket.c - php_connect_nonb(), used by ext/ftp for passive data connections So restoring to blocking matches what the POSIX code does: ```diff --- 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) \ ``` Async connects (STREAM_CLIENT_ASYNC_CONNECT) don't call RESTORE, so they are unaffected. I tested the patch on a local 8.5.11 NTS x64 build: - **Repro script:** the expected output above. - **Read timeout:** after stream_set_timeout($c, 0, 200000), fread() still times out after about 200 ms with timed_out => true. - **Non-blocking mode:** after stream_set_blocking($c, false), fread() still returns immediately. - **Connects:** connect latency and refused/timed-out connects are unchanged. I'll open a PR for it. ### PHP Version ```plain PHP 8.5.11 (cli) (built: Sep 22 2026 13:51:38) (NTS Visual C++ 2022 x64) Copyright (c) The PHP Group Built by The PHP Group Zend Engine v4.5.11, Copyright (c) Zend Technologies with Xdebug v3.5.3, Copyright (c) 2002-2026, by Derick Rethans with Zend OPcache v8.5.11, Copyright (c), by Zend Technologies ``` ### Operating System Windows 11 Pro 10.0.26300

« previous php.bugs (#252933) next »