Hacktoberfest 2026:維護者為十月標記出來的 issue,仍然開放、適合新手。 瀏覽 Hacktoberfest issue

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

未關閉 適合新手
#24,173 0 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

維護者通常 1 天內回覆

@vibbow 已經在處理了。

開始於 2026年10月7日。

  • #24174 來自 @vibbow —— 未關閉

評估

難度
2/5
預估耗時
1-3 小時
新手友好度
66/100
Issue 類型
缺陷
描述清晰度
描述清楚
活躍度
活躍
技術堆疊
c, php

研究方向

從 main/network.c 開始,檢查 php_network_connect_socket() 使用的 Windows 巨集 SET_SOCKET_BLOCKING_MODE 和 RESTORE_SOCKET_BLOCKING_MODE。Issue 包含一項提議的變更和一個重現指令碼;請確認同步連線會還原阻塞模式,而非同步連線不受影響,接著在 Windows 上執行相關的網路與串流測試。完成標準是,回報的讀取操作會等待資料,同時不會破壞逾時或非阻塞行為。

由索引模型根據 Issue 內容生成。

描述

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

主要語言
C
星號
40.4k
分支
8.2k
平均合併
1 天 21 小時
30 天內合併 PR
155

環境準備

  • 沒有 Dockerfile 或 Docker Compose 檔案
  • 沒有 Pull Request 範本
  • 閱讀貢獻指南

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

php/php-src 的其他 Issue

查看 php/php-src 的全部 Issue

相似的 Issue

更多 C Issue

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。