]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-145030: Fix asyncio write pipe transport for named FIFOs on macOS (#154222)
authorBhuvi <b.chouksey27@gmail.com>
Fri, 24 Jul 2026 13:53:51 +0000 (19:23 +0530)
committerGitHub <noreply@github.com>
Fri, 24 Jul 2026 13:53:51 +0000 (19:23 +0530)
Lib/asyncio/unix_events.py
Lib/test/test_asyncio/test_events.py
Misc/NEWS.d/next/Library/2026-07-20-12-40-00.gh-issue-145030.TFJm6k.rst [new file with mode: 0644]

index 1a7d534261ed72029a9cd5a5d6fd4198b162f07c..bb98f0014f81760b5bf78f45b4d38175c111f50c 100644 (file)
@@ -640,7 +640,8 @@ class _UnixWritePipeTransport(transports._FlowControlMixin,
         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 @@ class _UnixWritePipeTransport(transports._FlowControlMixin,
         # 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_apple = (
+            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):
             # only start reading when connection_made() has been called
             self._loop.call_soon(self._loop._add_reader,
                                  self._fileno, self._read_ready)
index 91882f41d9428ed77bfaa48d0c0ec09d6091dc6d..3fdfffdb213efc20f4741a66f971854c73f51373 100644 (file)
@@ -1725,6 +1725,48 @@ class EventLoopTestsMixin:
         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.assertNotEqual(os.stat(path).st_nlink, 0)
+        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 (file)
index 0000000..2c6b07a
--- /dev/null
@@ -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.