From 2c7545fa45846c9b19c46af3a2245e58235d1626 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 24 Jun 2025 10:54:09 +0300 Subject: [PATCH 1/3] gh-135836: Fix IndexError in asyncio.create_connection() It occurs when non-OSError exception is raised during connection and socket's close() raises OSError. --- Lib/asyncio/base_events.py | 62 ++++++++++--------- Lib/test/test_asyncio/test_base_events.py | 30 +++++++++ ...-06-24-10-52-35.gh-issue-135836.s37351.rst | 3 + 3 files changed, 65 insertions(+), 30 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-06-24-10-52-35.gh-issue-135836.s37351.rst diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 04fb961e9985e4..c356ebf847724a 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -1016,38 +1016,40 @@ async def _connect_sock(self, exceptions, addr_info, local_addr_infos=None): family, type_, proto, _, address = addr_info sock = None try: - sock = socket.socket(family=family, type=type_, proto=proto) - sock.setblocking(False) - if local_addr_infos is not None: - for lfamily, _, _, _, laddr in local_addr_infos: - # skip local addresses of different family - if lfamily != family: - continue - try: - sock.bind(laddr) - break - except OSError as exc: - msg = ( - f'error while attempting to bind on ' - f'address {laddr!r}: {str(exc).lower()}' - ) - exc = OSError(exc.errno, msg) - my_exceptions.append(exc) - else: # all bind attempts failed - if my_exceptions: - raise my_exceptions.pop() - else: - raise OSError(f"no matching local address with {family=} found") - await self.sock_connect(sock, address) - return sock - except OSError as exc: - my_exceptions.append(exc) - if sock is not None: - sock.close() - raise + try: + sock = socket.socket(family=family, type=type_, proto=proto) + sock.setblocking(False) + if local_addr_infos is not None: + for lfamily, _, _, _, laddr in local_addr_infos: + # skip local addresses of different family + if lfamily != family: + continue + try: + sock.bind(laddr) + break + except OSError as exc: + msg = ( + f'error while attempting to bind on ' + f'address {laddr!r}: {str(exc).lower()}' + ) + exc = OSError(exc.errno, msg) + my_exceptions.append(exc) + else: # all bind attempts failed + if my_exceptions: + raise my_exceptions.pop() + else: + raise OSError(f"no matching local address with {family=} found") + await self.sock_connect(sock, address) + return sock + except OSError as exc: + my_exceptions.append(exc) + raise except: if sock is not None: - sock.close() + try: + sock.close() + except OSError: + pass raise finally: exceptions = my_exceptions = None diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index 2ca5c4c6719c41..99b0d0530b80a9 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -3,6 +3,7 @@ import concurrent.futures import errno import math +import os import platform import socket import sys @@ -24,6 +25,10 @@ MOCK_ANY = mock.ANY +class CustomError(Exception): + pass + + def tearDownModule(): asyncio._set_event_loop_policy(None) @@ -1296,6 +1301,31 @@ def getaddrinfo_task(*args, **kwds): self.assertEqual(len(cm.exception.exceptions), 1) self.assertIsInstance(cm.exception.exceptions[0], OSError) + def test_create_connection_connect_non_os_err_close_err(self): + # Test the case when sock_connect() raises non-OSError exception + # and sock.close() raises OSError. + async def getaddrinfo(*args, **kw): + return [(2, 1, 6, '', ('107.6.106.82', 80))] + + def getaddrinfo_task(*args, **kwds): + return self.loop.create_task(getaddrinfo(*args, **kwds)) + + async def sock_connect(sock, address): + # Force sock.close() to raise OSError. + os.close(sock.fileno()) + raise CustomError + + self.loop.getaddrinfo = getaddrinfo_task + self.loop.sock_connect = sock_connect + + coro = self.loop.create_connection(MyProto, 'example.com', 80) + self.assertRaises( + CustomError, self.loop.run_until_complete, coro) + + coro = self.loop.create_connection(MyProto, 'example.com', 80, all_errors=True) + self.assertRaises( + CustomError, self.loop.run_until_complete, coro) + def test_create_connection_multiple(self): async def getaddrinfo(*args, **kw): return [(2, 1, 6, '', ('0.0.0.1', 80)), diff --git a/Misc/NEWS.d/next/Library/2025-06-24-10-52-35.gh-issue-135836.s37351.rst b/Misc/NEWS.d/next/Library/2025-06-24-10-52-35.gh-issue-135836.s37351.rst new file mode 100644 index 00000000000000..1d1e7a2298c085 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-24-10-52-35.gh-issue-135836.s37351.rst @@ -0,0 +1,3 @@ +Fix :exc:`IndexError` in :meth:`asyncio.loop.create_connection` that could +occur when non-\ :exc:`OSError` exception is raised during connection and +socket's ``close()`` raises :exc:`!OSError`. From 2cb4cae9f517e1a38ea043190e08bbc0eafcc701 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 24 Jun 2025 13:04:58 +0300 Subject: [PATCH 2/3] Make the test working on Windows. --- Lib/test/test_asyncio/test_base_events.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index 99b0d0530b80a9..ffa565e2ca4bc0 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -3,7 +3,6 @@ import concurrent.futures import errno import math -import os import platform import socket import sys @@ -1301,7 +1300,8 @@ def getaddrinfo_task(*args, **kwds): self.assertEqual(len(cm.exception.exceptions), 1) self.assertIsInstance(cm.exception.exceptions[0], OSError) - def test_create_connection_connect_non_os_err_close_err(self): + @patch_socket + def test_create_connection_connect_non_os_err_close_err(self, m_socket): # Test the case when sock_connect() raises non-OSError exception # and sock.close() raises OSError. async def getaddrinfo(*args, **kw): @@ -1310,13 +1310,12 @@ async def getaddrinfo(*args, **kw): def getaddrinfo_task(*args, **kwds): return self.loop.create_task(getaddrinfo(*args, **kwds)) - async def sock_connect(sock, address): - # Force sock.close() to raise OSError. - os.close(sock.fileno()) - raise CustomError - self.loop.getaddrinfo = getaddrinfo_task - self.loop.sock_connect = sock_connect + self.loop.sock_connect = mock.Mock() + self.loop.sock_connect.side_effect = CustomError + sock = mock.Mock() + m_socket.socket.return_value = sock + sock.close.side_effect = OSError coro = self.loop.create_connection(MyProto, 'example.com', 80) self.assertRaises( From c0b301ff1bb525695199a8a2b81ad7020d404875 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 2 Jul 2025 22:06:03 +0300 Subject: [PATCH 3/3] Add a comment. --- Lib/asyncio/base_events.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index ec6499d198b8a5..520d4b398545bf 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -1049,6 +1049,9 @@ async def _connect_sock(self, exceptions, addr_info, local_addr_infos=None): try: sock.close() except OSError: + # An error when closing a newly created socket is + # not important, but it can overwrite more important + # non-OSError error. So ignore it. pass raise finally: