Sockets from synchronous stream_socket_client() are left in non-blocking mode on Windows
Les mainteneurs répondent en général sous 1 jour
É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
- 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
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()returnsfalseat once.socket_import_stream()marks the imported socket as blocking (it copies the stream'sis_blocked), butsocket_read()/socket_recv()fail immediately withWSAEWOULDBLOCK(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 withtimed_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
- Aucun Dockerfile ni fichier Docker Compose
- Aucun modèle de pull request
- Lire le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de php/php-src
-
Feature Status: Needs Triage
Difficulté 2/5 1-3 heures Accessibilité débutants 73/100
Les mainteneurs répondent en général sous 1 jour
-
Variant analysis: 1 unfixed sibling safety gap in php-srcPeut-être pris @kamil-tekiela l’a pris il y a 9 jours. Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 72/100
php/php-src#23958 · 1 personne assignée ·
Les mainteneurs répondent en général sous 1 jour
-
sapi_lsapi_ub_write does not return bytes written in lsapi modePeut-être pris Une pull request liée à cette issue est ouverte ou déjà fusionnée. OuverteBug Status: Needs Triage
Difficulté 1/5 Moins d'une heure Accessibilité débutants 90/100
Les mainteneurs répondent en général sous 1 jour
-
Bug Status: Needs Triage
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
Les mainteneurs répondent en général sous 1 jour
-
Use of PIDFile= in php-fpm.servicePeut-être pris @CodedByManish l’a pris il y a 26 jours. OuverteBug SAPI: fpm Status: Needs Triage
Difficulté 2/5 1-3 heures Accessibilité débutants 65/100
php/php-src#21740 · 1 réaction ·
Les mainteneurs répondent en général sous 1 jour
Toutes les issues de php/php-src
Issues similaires
-
rc_runtime_activate_richpresence leaves a half-initialised entry when the buffer allocation failsOuverte
Difficulté 1/5 Moins d'une heure Accessibilité débutants 88/100
RetroAchievements/rcheevos#558 ·
-
good first issue priority:low type:docs
Difficulté 2/5 1-3 heures Accessibilité débutants 82/100
crazy-goat/php-fpm-ng#920 ·
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 74/100
-
Difficulté 2/5 1-3 heures Accessibilité débutants 88/100
libsdl-org/SDL#16464 ·
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100