From 8b24d7adc899b9c0e5329130363bfe3b9ccf7961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cbhuvi27=E2=80=9D?= Date: Mon, 20 Jul 2026 12:39:32 +0530 Subject: [PATCH 1/3] gh-145030: Fix asyncio write pipe transport for named FIFOs on macOS On macOS the write end of a named FIFO polls as readable whenever unread data sits in the FIFO, so the reader trick used to detect the peer closing misfired and closed the transport. The trick also cannot detect a genuine hangup there: closing the read end delivers no event and kqueue's EV_EOF tracks writers, not readers. Skip registering the reader for named FIFOs on macOS, like the existing AIX carve-out; a closed peer is still detected on the next write via EPIPE. Anonymous os.pipe() pipes (st_nlink == 0) keep the current behaviour. --- Lib/asyncio/unix_events.py | 14 ++++++- Lib/test/test_asyncio/test_events.py | 41 +++++++++++++++++++ ...-07-20-12-40-00.gh-issue-145030.TFJm6k.rst | 3 ++ 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index d436512d6c45fc5..2177157a1bb5cf2 100644 --- a/Lib/asyncio/unix_events.py +++ b/Lib/asyncio/unix_events.py @@ -640,7 +640,8 @@ def __init__(self, loop, pipe, protocol, waiter=None, extra=None): self._conn_lost = 0 self._closing = False # Set when close() or write_eof() called. - mode = os.fstat(self._fileno).st_mode + pipe_stat = os.fstat(self._fileno) + mode = pipe_stat.st_mode is_char = stat.S_ISCHR(mode) is_fifo = stat.S_ISFIFO(mode) is_socket = stat.S_ISSOCK(mode) @@ -657,7 +658,16 @@ def __init__(self, loop, pipe, protocol, waiter=None, extra=None): # On AIX, the reader trick (to be notified when the read end of the # socket is closed) only works for sockets. On other platforms it # works for pipes and sockets. (Exception: OS X 10.4? Issue #19294.) - if is_socket or (is_fifo and not sys.platform.startswith("aix")): + # On macOS, the trick misfires for named FIFOs (but not for pipes + # created with os.pipe(), which have st_nlink == 0): the write end + # polls as readable whenever unread data sits in the FIFO, and no + # event is delivered when the read end is closed, so it can only + # ever report a false disconnection (gh-145030). + is_named_fifo_on_macos = (sys.platform == "darwin" + and is_fifo and pipe_stat.st_nlink > 0) + if is_socket or (is_fifo + and not sys.platform.startswith("aix") + and not is_named_fifo_on_macos): # only start reading when connection_made() has been called self._loop.call_soon(self._loop._add_reader, self._fileno, self._read_ready) diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py index 91882f41d9428ed..aaeaf85adbda8cc 100644 --- a/Lib/test/test_asyncio/test_events.py +++ b/Lib/test/test_asyncio/test_events.py @@ -1725,6 +1725,47 @@ def reader(data): self.loop.run_until_complete(proto.done) self.assertEqual('CLOSED', proto.state) + @unittest.skipUnless(sys.platform != 'win32', + "Don't support pipes for Windows") + @unittest.skipUnless(hasattr(os, 'mkfifo'), 'requires os.mkfifo()') + def test_write_named_fifo_unread_data(self): + # gh-145030: on macOS, the write end of a named FIFO polls as + # readable while unread data sits in the FIFO, which made the + # transport misinterpret the event as the reader hanging up + # and close itself. + path = os_helper.TESTFN + os.mkfifo(path) + self.addCleanup(os_helper.unlink, path) + rfd = os.open(path, os.O_RDONLY | os.O_NONBLOCK) + self.addCleanup(os.close, rfd) + wfd = os.open(path, os.O_WRONLY | os.O_NONBLOCK) + pipeobj = io.open(wfd, 'wb', 1024) + + proto = MyWritePipeProto(loop=self.loop) + connect = self.loop.connect_write_pipe(lambda: proto, pipeobj) + transport, p = self.loop.run_until_complete(connect) + self.assertIs(p, proto) + self.assertEqual('CONNECTED', proto.state) + + transport.write(b'1') + # Iterate the event loop while the data stays unread in the FIFO; + # the transport must not detect a false disconnection. + for _ in range(10): + test_utils.run_briefly(self.loop) + self.assertEqual('CONNECTED', proto.state) + self.assertFalse(transport.is_closing()) + self.assertEqual(b'1', os.read(rfd, 1024)) + + transport.write(b'2345') + for _ in range(10): + test_utils.run_briefly(self.loop) + self.assertEqual('CONNECTED', proto.state) + self.assertEqual(b'2345', os.read(rfd, 1024)) + + transport.close() + self.loop.run_until_complete(proto.done) + self.assertEqual('CLOSED', proto.state) + @unittest.skipUnless(sys.platform != 'win32', "Don't support pipes for Windows") def test_write_pipe_disconnect_on_close(self): diff --git a/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst b/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst new file mode 100644 index 000000000000000..2c6b07a4fa8b76f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst @@ -0,0 +1,3 @@ +Fix :mod:`asyncio` write pipe transports for named FIFOs on macOS. Unread +data sitting in the FIFO made the transport misinterpret a poll event as +the reader disconnecting, wrongly closing the transport. From f58380f5f7e186d0d9fc791ef893095665dee43c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cbhuvi27=E2=80=9D?= Date: Mon, 20 Jul 2026 13:52:01 +0530 Subject: [PATCH 2/3] gh-145030: Also skip FIFO reader trick on iOS/tvOS/watchOS sys.platform is "ios" (etc.) on Apple mobile, not "darwin", so the named-FIFO carve-out never applied there and the new regression test failed on the iOS CI job. Match all Apple platforms. --- Lib/asyncio/unix_events.py | 18 ++++++++++-------- Lib/test/test_asyncio/test_events.py | 8 ++++---- ...6-07-20-12-40-00.gh-issue-145030.TFJm6k.rst | 6 +++--- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index 2177157a1bb5cf2..09caf67f442b266 100644 --- a/Lib/asyncio/unix_events.py +++ b/Lib/asyncio/unix_events.py @@ -658,16 +658,18 @@ def __init__(self, loop, pipe, protocol, waiter=None, extra=None): # On AIX, the reader trick (to be notified when the read end of the # socket is closed) only works for sockets. On other platforms it # works for pipes and sockets. (Exception: OS X 10.4? Issue #19294.) - # On macOS, the trick misfires for named FIFOs (but not for pipes - # created with os.pipe(), which have st_nlink == 0): the write end - # polls as readable whenever unread data sits in the FIFO, and no - # event is delivered when the read end is closed, so it can only - # ever report a false disconnection (gh-145030). - is_named_fifo_on_macos = (sys.platform == "darwin" - and is_fifo and pipe_stat.st_nlink > 0) + # On Apple platforms (macOS/iOS/tvOS/watchOS), the trick misfires for + # named FIFOs (but not for pipes created with os.pipe(), which have + # st_nlink == 0): the write end polls as readable whenever unread + # data sits in the FIFO, and no event is delivered when the read end + # is closed, so it can only ever report a false disconnection + # (gh-145030). + is_apple = sys.platform in {"darwin", "ios", "tvos", "watchos"} + is_named_fifo_on_apple = (is_apple and is_fifo + and pipe_stat.st_nlink > 0) if is_socket or (is_fifo and not sys.platform.startswith("aix") - and not is_named_fifo_on_macos): + and not is_named_fifo_on_apple): # only start reading when connection_made() has been called self._loop.call_soon(self._loop._add_reader, self._fileno, self._read_ready) diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py index aaeaf85adbda8cc..781b80b9ebc3a0c 100644 --- a/Lib/test/test_asyncio/test_events.py +++ b/Lib/test/test_asyncio/test_events.py @@ -1729,10 +1729,10 @@ def reader(data): "Don't support pipes for Windows") @unittest.skipUnless(hasattr(os, 'mkfifo'), 'requires os.mkfifo()') def test_write_named_fifo_unread_data(self): - # gh-145030: on macOS, the write end of a named FIFO polls as - # readable while unread data sits in the FIFO, which made the - # transport misinterpret the event as the reader hanging up - # and close itself. + # gh-145030: on Apple platforms, the write end of a named FIFO + # polls as readable while unread data sits in the FIFO, which + # made the transport misinterpret the event as the reader hanging + # up and close itself. path = os_helper.TESTFN os.mkfifo(path) self.addCleanup(os_helper.unlink, path) diff --git a/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst b/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst index 2c6b07a4fa8b76f..74b4d62afe947c0 100644 --- a/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst +++ b/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst @@ -1,3 +1,3 @@ -Fix :mod:`asyncio` write pipe transports for named FIFOs on macOS. Unread -data sitting in the FIFO made the transport misinterpret a poll event as -the reader disconnecting, wrongly closing the transport. +Fix :mod:`asyncio` write pipe transports for named FIFOs on Apple platforms. +Unread data sitting in the FIFO made the transport misinterpret a poll +event as the reader disconnecting, wrongly closing the transport. From 094c1d48f6e78b772ed5f7ed76ec2703b2405aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cbhuvi27=E2=80=9D?= Date: Mon, 20 Jul 2026 13:59:08 +0530 Subject: [PATCH 3/3] gh-145030: Prefer is_named_fifo_on_macos naming Rename the carve-out variable and wording back to macOS; keep the platform set covering ios/tvos/watchos so the same xnu FIFO behaviour is handled there. --- Lib/asyncio/unix_events.py | 21 ++++++++++--------- Lib/test/test_asyncio/test_events.py | 8 +++---- ...-07-20-12-40-00.gh-issue-145030.TFJm6k.rst | 6 +++--- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index 09caf67f442b266..1490668f1e10f61 100644 --- a/Lib/asyncio/unix_events.py +++ b/Lib/asyncio/unix_events.py @@ -658,18 +658,19 @@ def __init__(self, loop, pipe, protocol, waiter=None, extra=None): # On AIX, the reader trick (to be notified when the read end of the # socket is closed) only works for sockets. On other platforms it # works for pipes and sockets. (Exception: OS X 10.4? Issue #19294.) - # On Apple platforms (macOS/iOS/tvOS/watchOS), the trick misfires for - # named FIFOs (but not for pipes created with os.pipe(), which have - # st_nlink == 0): the write end polls as readable whenever unread - # data sits in the FIFO, and no event is delivered when the read end - # is closed, so it can only ever report a false disconnection - # (gh-145030). - is_apple = sys.platform in {"darwin", "ios", "tvos", "watchos"} - is_named_fifo_on_apple = (is_apple and is_fifo - and pipe_stat.st_nlink > 0) + # On macOS, the trick misfires for named FIFOs (but not for pipes + # created with os.pipe(), which have st_nlink == 0): the write end + # polls as readable whenever unread data sits in the FIFO, and no + # event is delivered when the read end is closed, so it can only + # ever report a false disconnection (gh-145030). The same xnu + # behaviour applies on iOS/tvOS/watchOS (sys.platform is not + # "darwin" there). + is_named_fifo_on_macos = ( + sys.platform in {"darwin", "ios", "tvos", "watchos"} + and is_fifo and pipe_stat.st_nlink > 0) if is_socket or (is_fifo and not sys.platform.startswith("aix") - and not is_named_fifo_on_apple): + and not is_named_fifo_on_macos): # only start reading when connection_made() has been called self._loop.call_soon(self._loop._add_reader, self._fileno, self._read_ready) diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py index 781b80b9ebc3a0c..aaeaf85adbda8cc 100644 --- a/Lib/test/test_asyncio/test_events.py +++ b/Lib/test/test_asyncio/test_events.py @@ -1729,10 +1729,10 @@ def reader(data): "Don't support pipes for Windows") @unittest.skipUnless(hasattr(os, 'mkfifo'), 'requires os.mkfifo()') def test_write_named_fifo_unread_data(self): - # gh-145030: on Apple platforms, the write end of a named FIFO - # polls as readable while unread data sits in the FIFO, which - # made the transport misinterpret the event as the reader hanging - # up and close itself. + # gh-145030: on macOS, the write end of a named FIFO polls as + # readable while unread data sits in the FIFO, which made the + # transport misinterpret the event as the reader hanging up + # and close itself. path = os_helper.TESTFN os.mkfifo(path) self.addCleanup(os_helper.unlink, path) diff --git a/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst b/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst index 74b4d62afe947c0..2c6b07a4fa8b76f 100644 --- a/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst +++ b/Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst @@ -1,3 +1,3 @@ -Fix :mod:`asyncio` write pipe transports for named FIFOs on Apple platforms. -Unread data sitting in the FIFO made the transport misinterpret a poll -event as the reader disconnecting, wrongly closing the transport. +Fix :mod:`asyncio` write pipe transports for named FIFOs on macOS. Unread +data sitting in the FIFO made the transport misinterpret a poll event as +the reader disconnecting, wrongly closing the transport.