From ede808c3cb188f14668d4bc282a7d9d12a022792 Mon Sep 17 00:00:00 2001 From: Aslan Osmanov Date: Sat, 24 Oct 2026 14:11:07 +0000 Subject: [PATCH] gh-160429: Always close the socket in proactor _call_connection_lost() A ConnectionError raised by the socket shutdown call (e.g. WinError 10054 when the peer has reset the connection) escaped before the socket was closed, the transport was detached from its server and _called_connection_lost was set. Ignore connection errors there and run the rest of the cleanup in a finally block. --- Lib/asyncio/proactor_events.py | 34 ++++++++++++------- Lib/test/test_asyncio/test_proactor_events.py | 30 ++++++++++++++++ ...-10-24-15-30-00.gh-issue-160429.Rk3vQd.rst | 3 ++ 3 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-24-15-30-00.gh-issue-160429.Rk3vQd.rst diff --git a/Lib/asyncio/proactor_events.py b/Lib/asyncio/proactor_events.py index 8350e0276046884..db90ceefb44911d 100644 --- a/Lib/asyncio/proactor_events.py +++ b/Lib/asyncio/proactor_events.py @@ -159,19 +159,27 @@ def _call_connection_lost(self, exc): try: self._protocol.connection_lost(exc) finally: - # XXX If there is a pending overlapped read on the other - # end then it may fail with ERROR_NETNAME_DELETED if we - # just close our end. First calling shutdown() seems to - # cure it, but maybe using DisconnectEx() would be better. - if hasattr(self._sock, 'shutdown') and self._sock.fileno() != -1: - self._sock.shutdown(socket.SHUT_RDWR) - self._sock.close() - self._sock = None - server = self._server - if server is not None: - server._detach(self) - self._server = None - self._called_connection_lost = True + try: + # XXX If there is a pending overlapped read on the other + # end then it may fail with ERROR_NETNAME_DELETED if we + # just close our end. First calling shutdown() seems to + # cure it, but maybe using DisconnectEx() would be better. + if (hasattr(self._sock, 'shutdown') + and self._sock.fileno() != -1): + try: + self._sock.shutdown(socket.SHUT_RDWR) + except ConnectionError: + # The peer has already reset the connection. + pass + finally: + # Always release the socket, even if shutdown() failed. + self._sock.close() + self._sock = None + server = self._server + if server is not None: + server._detach(self) + self._server = None + self._called_connection_lost = True def get_write_buffer_size(self): size = self._pending_write diff --git a/Lib/test/test_asyncio/test_proactor_events.py b/Lib/test/test_asyncio/test_proactor_events.py index 2f229887ad62cc8..4c49ee455c22e78 100644 --- a/Lib/test/test_asyncio/test_proactor_events.py +++ b/Lib/test/test_asyncio/test_proactor_events.py @@ -336,6 +336,36 @@ def test_call_connection_lost(self): self.assertTrue(self.protocol.connection_lost.called) self.assertTrue(self.sock.close.called) + def test_call_connection_lost_shutdown_reset(self): + # gh-160429: a connection reset by the peer must not prevent + # the socket from being closed and the transport from being + # detached from its server. + server = mock.Mock() + tr = _ProactorSocketTransport(self.loop, self.sock, self.protocol, + server=server) + self.sock.shutdown.side_effect = ConnectionResetError + tr._call_connection_lost(None) + self.protocol.connection_lost.assert_called_once_with(None) + self.sock.shutdown.assert_called_once_with(socket.SHUT_RDWR) + self.sock.close.assert_called_once_with() + server._detach.assert_called_once_with(tr) + self.assertIsNone(tr._sock) + self.assertIsNone(tr._server) + self.assertTrue(tr._called_connection_lost) + + def test_call_connection_lost_shutdown_error(self): + # Unexpected shutdown() errors still propagate, but the socket is + # closed anyway. + server = mock.Mock() + tr = _ProactorSocketTransport(self.loop, self.sock, self.protocol, + server=server) + self.sock.shutdown.side_effect = OSError + with self.assertRaises(OSError): + tr._call_connection_lost(None) + self.sock.close.assert_called_once_with() + server._detach.assert_called_once_with(tr) + self.assertTrue(tr._called_connection_lost) + def test_write_eof(self): tr = self.socket_transport() self.assertTrue(tr.can_write_eof()) diff --git a/Misc/NEWS.d/next/Library/2026-10-24-15-30-00.gh-issue-160429.Rk3vQd.rst b/Misc/NEWS.d/next/Library/2026-10-24-15-30-00.gh-issue-160429.Rk3vQd.rst new file mode 100644 index 000000000000000..3bfa3e9b49e21d4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-24-15-30-00.gh-issue-160429.Rk3vQd.rst @@ -0,0 +1,3 @@ +Fix :mod:`asyncio` proactor transports skipping the closing of the socket +and the detaching from the server when the socket shutdown fails because +the peer has already reset the connection.