Hacktoberfest 2026 : les issues que les mainteneurs ont marquées pour octobre, ouvertes et accessibles aux débutants. Parcourir les issues Hacktoberfest

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

Ouverte Adaptée aux débutants
#24,173 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Les mainteneurs répondent en général sous 1 jour

@vibbow y travaille déjà.

Depuis le 7/10/2026.

  • #24174 par @vibbow — ouverte

Évaluation

Difficulté
2/5
Temps estimé
1-3 heures
Accessibilité débutants
66/100
Type d'issue
Bug
Clarté
Clairement spécifiée
Activité
Active
Stack technique
c, php
Domaine
backend, networking

Piste de recherche

Commencez dans main/network.c et examinez les macros Windows SET_SOCKET_BLOCKING_MODE et RESTORE_SOCKET_BLOCKING_MODE utilisées par php_network_connect_socket(). L’issue comprend une modification proposée et un script de reproduction ; vérifiez que les connexions synchrones restaurent le mode bloquant, tandis que les connexions asynchrones restent inchangées, puis exécutez les tests réseau et stream pertinents sous Windows. Le critère de réussite est que les lectures signalées attendent les données sans perturber le comportement du délai d’expiration ou du mode non bloquant.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

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

Langage dominant
C
Étoiles
40.4k
Forks
8.2k
Merge moyen
1 j 23 h
PR mergées (30 j)
150

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de php/php-src

Toutes les issues de php/php-src

Issues similaires

Plus d'issues C

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.