Skip to content

Commit 51a03c8

Browse files
committed
gh-155934: Fix socket leak and silent error for asyncio accepted connections
1 parent c3df37c commit 51a03c8

3 files changed

Lines changed: 60 additions & 1 deletion

File tree

Lib/asyncio/selector_events.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,9 @@ async def _accept_connection2(
253253
except (SystemExit, KeyboardInterrupt):
254254
raise
255255
except BaseException as exc:
256-
if self._debug:
256+
if transport is None:
257+
conn.close()
258+
if transport is None or self._debug:
257259
context = {
258260
'message':
259261
'Error on transport creation for incoming connection',

Lib/test/test_asyncio/test_selector_events.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,60 @@ def test_accept_connection_reschedules_once_on_resource_error(self):
421421
self.assertEqual(self.loop.call_exception_handler.call_count, 1)
422422
self.assertEqual(self.loop.call_later.call_count, 1)
423423

424+
def test_accept_connection2_factory_error_closes_conn(self):
425+
# gh-155934: if the transport was never created, the accepted
426+
# socket is closed and the error is reported even when debug
427+
# mode is disabled.
428+
self.loop.set_debug(False)
429+
conn = mock.Mock()
430+
431+
def factory():
432+
raise RuntimeError("protocol_factory failed")
433+
434+
self.loop.call_exception_handler = mock.Mock()
435+
self.loop.run_until_complete(
436+
self.loop._accept_connection2(factory, conn, {}))
437+
438+
self.assertTrue(conn.close.called)
439+
self.loop.call_exception_handler.assert_called_once()
440+
441+
def test_accept_connection2_transport_error_closes_conn(self):
442+
# gh-155934: same when the transport creation itself fails.
443+
self.loop.set_debug(False)
444+
conn = mock.Mock()
445+
self.loop._make_socket_transport = mock.Mock(
446+
side_effect=ZeroDivisionError)
447+
self.loop.call_exception_handler = mock.Mock()
448+
449+
self.loop.run_until_complete(
450+
self.loop._accept_connection2(mock.Mock(), conn, {}))
451+
452+
self.assertTrue(conn.close.called)
453+
self.loop.call_exception_handler.assert_called_once()
454+
455+
def test_accept_connection2_waiter_error_stays_debug_only(self):
456+
# Once the transport exists it owns the socket: waiter failures
457+
# (e.g. SSL handshake errors) close the transport and stay
458+
# debug-only, and the accepted socket is not closed directly.
459+
self.loop.set_debug(False)
460+
conn = mock.Mock()
461+
transport = mock.Mock()
462+
463+
def make_transport(conn, protocol, waiter=None, **kwargs):
464+
waiter.set_exception(OSError("handshake failed"))
465+
return transport
466+
467+
self.loop._make_socket_transport = make_transport
468+
self.loop.call_exception_handler = mock.Mock()
469+
470+
self.loop.run_until_complete(
471+
self.loop._accept_connection2(mock.Mock(), conn, {}))
472+
473+
self.assertTrue(transport.close.called)
474+
self.assertFalse(conn.close.called)
475+
self.assertFalse(self.loop.call_exception_handler.called)
476+
477+
424478
class SelectorTransportTests(test_utils.TestCase):
425479

426480
def setUp(self):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a socket leak in :mod:`asyncio` when transport creation fails for a
2+
connection accepted by a server, and report the error via the loop exception
3+
handler even when debug mode is disabled.

0 commit comments

Comments
 (0)