python / python/cpython

macOS: abortive close (SO_LINGER {1,0}) of a flow-controlled loopback socket occasionally loses the RST, hanging asyncio peers

未關閉
#153,117 3 則留言 1 個 reaction 已指派 0 人 在 GitHub 檢視

還沒有人認領這個 Issue。

OS-mac topic-socket type-bug
主要語言
Python
星號
77.2k
分支
36k
PR 合併指標
PR 指標待擷取

描述

Summary

On macOS, when a flow-controlled (zero receive window) loopback TCP connection is abortively closedsetsockopt(SO_LINGER, {1, 0}) then close() — the reset (RST) is occasionally never delivered to the peer. The peer's socket stays ESTABLISHED forever: its registered reader never fires, getpeername() still succeeds, SO_ERROR == 0, and recv(..., MSG_PEEK) returns EWOULDBLOCK. The connection is left half-open.

The closing side is textbook-correct at close() time (still connected, SO_LINGER set to {1,0}, unsent bytes in the send buffer), and its fd is released cleanly (lsof confirms no lingering fd). So per POSIX/BSD semantics close() must emit a RST — but the peer never sees it, nor a FIN.

This surfaces as a permanent, silent hang of otherwise-correct asyncio programs on macOS. It is ultimately a macOS kernel lost-RST (it reproduces with plain selectors, no asyncio), but asyncio's normal teardown pattern makes it markedly more frequent — see below.

Reproducer

Full runnable reproducers + red CI on macOS: https://github.com/graingert/asyncio-macos-loopback-rst-hang

Three variants, all stdlib-only:

  • repro.py — asyncio (loop.add_reader/add_writer + a call_soon-deferred abortive close)
  • repro_selectors.py — plain selectors, immediate close
  • repro_selectors_deferred.py — plain selectors, close deferred by one poll cycle

Each iteration: establish a loopback pair; the server never reads so the client's writes drive the window to zero and fill its send buffer; register the client fd, fill until send() blocks; unregister and abortively close the client; register the server and wait for the disconnect. If the disconnect never arrives within a timeout, the RST was lost.

Results (single ~10-minute CI run per cell; undelivered / total)

variant macOS 14 macOS 15 Linux
asyncio (deferred close) 19 / 1.46M 4 / 0.93M 0 / 1.1M
selectors, immediate close 5 / 1.55M 0 / 1.16M 0 / 1.3M
selectors, deferred close 21 / 1.89M 1 / 1.27M 0 / 1.3M

Rates are low (~1–13 per million) and noisy, so an occasional 0 does not mean "cannot reproduce."

Analysis

  1. It is not an asyncio logic bug — it reproduces with a plain selectors (KqueueSelector) loop and no asyncio (macOS 14, immediate close). The underlying lost RST is macOS kernel behavior.
  2. The deferred close amplifies it ~3–4×. "Deferred close" = a select()/kqueue poll cycle runs between unregistering the fd and abortively closing it:
    unregister(fd)  ->  select()  ->  setsockopt(SO_LINGER {1,0}); close(fd)
    
    The immediate-close variant (unregister(fd); close(fd) with no poll between) hits it much less often.
  3. asyncio always inserts that poll cycle, because a transport's close()/abort is deferred to a later loop iteration via call_soon. That is why asyncio programs on macOS hit this most, and hang permanently when they do (loop.sock_recv / a registered reader that never fires).
  4. Never observed on Linux (epoll) across many millions of iterations.

Environment

  • macOS 14 and macOS 15 (GitHub-hosted macos-14 / macos-15 runners, Apple Silicon), CPython 3.13.
  • Reproduces on the default SelectorEventLoop (KqueueSelector).
  • Does not reproduce on Linux.

Not a duplicate of

  • #77444 — "Asyncio server enters an invalid state after a request with SO_LINGER" (server-side accept/state; not a lost RST).
  • #71573 — "Asyncio server hang when clients connect and immediately disconnect" (server-side hang; different mechanism).

Those are server-side; this is a client-side abortive close whose RST is lost, leaving the peer half-open.

貢獻指南

開啟貢獻指南

從這裡開始

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

研究方向

從連結的重現儲存庫中的 repro.py 和 repro_selectors.py 開始,然後比較 macOS 上 asyncio 的延遲關閉路徑與一般 selectors 變體。該 issue 沒有指出需要對 CPython 進行的變更;有價值的結果應確定是否存在 CPython 端的緩解措施,或記錄 macOS 核心的限制,並由重現程式驗證結果。

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

評估

技術堆疊
macos, python
領域
networking
Issue 類型
缺陷
難度
5/5
預估耗時
一週以上
活躍度
冷清
描述清晰度
需要釐清
新手友好度
28/100

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

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