`connect_read_pipe` double-closes the underlying fd, possibly closing an unrelated fd
まだ誰も着手していません。
評価
- 難易度
- 3/5
- 見積もり時間
- 1〜2日
- 初心者へのやさしさ
- 68/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- python
- 領域
- networking
調査の方向性
トランスポートのエントリポイントである connect_read_pipe と connect_write_pipe から始め、それらのファイルディスクリプタのクリーンアップを、コミット d5195d7c10fbae81ef1dcb4609cee50f8aa746fa のソケット修正と比較します。提供された再現コードを uvloop と標準の asyncio ループで実行します。いずれかのパイプトランスポートを閉じるかガベージコレクションしても、無関係なファイルディスクリプタが閉じられなければ完了です。
索引モデルが issue の本文から書いたものです。
説明
In our production use of uvloop, we found sporadic cases of file descriptors being randomly closed. (For more details of our specific case, see skypilot-org/skypilot#10681).
We traced this back to loop.connect_read_pipe double-closing its file descriptor. When the fd was re-allocated between the first and second close, this caused an unrelated thread to have its fd closed out from under.
I used this script to check the double-close behavior:
import asyncio, io, os, sys
class BufferedReaderWithProbe(io.BufferedReader):
def close(self):
# Probe before status
fd = self.fileno()
print(f"closing {self} (fd {fd})")
try:
os.fstat(fd); fd_state = "still OPEN"
except OSError as e:
fd_state = f"already closed ({e.strerror})"
# Open a new pipe to see what fds we get
# Kernel should reuse lowest free number
newfd_reader, newfd_writer = os.pipe()
print(f" is fd {fd} already closed? {fd_state}")
print(f" fresh os.pipe() got fds {newfd_reader},{newfd_writer}")
# Do normal BufferedReader close
try:
print("calling BufferedReader.close")
super().close()
finally:
# Probe after status
print("Status after close:")
for fd in (newfd_reader, newfd_writer):
try:
os.fstat(fd); print(f" fresh fd {fd}: alive")
except OSError as e:
print(f" fresh fd {fd}: DEAD ({e.strerror}) <-- stolen")
async def main():
loop = asyncio.get_running_loop()
pipe_read_fd, pipe_write_fd = os.pipe()
probe_reader = BufferedReaderWithProbe(io.FileIO(pipe_read_fd, "rb"))
transport, _ = await loop.connect_read_pipe(asyncio.Protocol, probe_reader)
print(f"transport {type(transport).__name__} on fd {pipe_read_fd}")
transport.close()
if __name__ == "__main__":
if sys.argv[1:] == ["uvloop"]:
import uvloop; uvloop.install(); print(f"== uvloop {uvloop.__version__}")
else:
print(f"== stdlib asyncio {sys.version.split()[0]}")
asyncio.run(main())
$ python repro.py
== stdlib asyncio 3.10.16
transport _UnixReadPipeTransport on fd 6
closing <BufferedReaderWithProbe name=6> (fd 6)
is fd 6 already closed? still OPEN
fresh os.pipe() got fds 8,9
calling BufferedReader.close
Status after close:
fresh fd 8: alive
fresh fd 9: alive
$ python repro.py uvloop
== uvloop 0.22.1
transport ReadUnixTransport on fd 13
closing <BufferedReaderWithProbe name=13> (fd 13)
is fd 13 already closed? already closed (Bad file descriptor)
fresh os.pipe() got fds 13,16
calling BufferedReader.close
Status after close:
fresh fd 13: DEAD (Bad file descriptor) <-- stolen
fresh fd 16: alive
Here you can see that a asyncio event loop correctly leaves fd 6 open, allowing the underlying file object (in this case, our custom BufferedReader) to close the fd.
However, uvloop has already closed the fd, so when the file object is closed, it re-closes fd 13 which is now an unrelated os.pipe() fd.
This specific case was already fixed for sockets: https://github.com/MagicStack/uvloop/commit/d5195d7c10fbae81ef1dcb4609cee50f8aa746fa. It was assumed that the EBADF for non-sockets was benign, but it can actually be a problem if the underlying fd is reassigned before the _fileobj.close() can run.
You can observe the same behavior if the transport is not explicitly closed but is instead GC'd - although in this case it seems that the file object is closed before the uv_close call double-closes the fd. (The order is inverted.) This is actually the case we saw in prod.
It also looks like connect_write_pipe is similarly affected.
AI disclosure: This investigation was heavily assisted by AI tools, but this report was entirely human written without assistance except for the original repro script. The original repro script was then modified by me for clarity and manually validated.
- 主要言語
- Cython
- スター
- 11.9k
- フォーク
- 615
- PR マージ指標
- 30日以内にマージされた PR はありません
コントリビューションガイド
このリポジトリのコントリビューションガイドは索引されていません
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
MagicStack/uvloop のほかの issue
-
License not clear オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
MagicStack/uvloop#759 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
MagicStack/uvloop#741 · リアクション 2 件 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
MagicStack/uvloop#702 · コメント 8 件 · リアクション 9 件 ·
-
難易度 3/5 1〜2日 初心者へのやさしさ 68/100
MagicStack/uvloop#760 ·
-
難易度 4/5 3〜5日 初心者へのやさしさ 48/100
MagicStack/uvloop#754 · コメント 1 件 · リアクション 1 件 ·
MagicStack/uvloop の issue をすべて見る
似ている issue
-
Bug Java Platform: Java
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
getsentry/sentry-java#6138 · コメント 1 件 ·
-
kind/bug Ubuntu 24
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
kubernetes-sigs/kubespray#13532 ·
-
language/en needs-triage sig/network
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
kubernetes/website#57642 · コメント 1 件 ·
-
app.loanspq.com オープンN: AdGuard DNS P3: Medium T: Incorrect Blocking
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
AdguardTeam/AdguardFilters#242018 ·
-
[adam] AdamNet network read doesn't cap to MAX_ADAM_PACKET_LEN, overflows client receive buffers オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
FujiNetWIFI/fujinet-firmware#1649 · コメント 2 件 ·