diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index d436512d6c45fc..1490668f1e10f6 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,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.) - 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). 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_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 91882f41d9428e..aaeaf85adbda8c 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 00000000000000..2c6b07a4fa8b76 --- /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.