Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Sockets from synchronous stream_socket_client() are left in non-blocking mode on Windows

Open Beginner friendly
#24,173 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
66/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
c, php

Research direction

Start in main/network.c and inspect the Windows SET_SOCKET_BLOCKING_MODE and RESTORE_SOCKET_BLOCKING_MODE macros used by php_network_connect_socket(). The issue includes a proposed change and a repro script; verify synchronous connects restore blocking mode while asynchronous connects remain unaffected, then run the relevant network and stream tests on Windows. Done means the reported reads wait for data without breaking timeout or non-blocking behavior.

Written by the indexing model from the issue text.

Description

Bug Status: Needs Triage
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
// 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:

#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:

--- 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
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

Dominant language
C
Stars
40.4k
Forks
8.2k
Avg merge
2d 1h
Merged PRs (30d)
153

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from php/php-src

All issues in php/php-src

Similar issues

More C issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.