Skip to content

Commit a608d2d

Browse files
fafhrd911st1
authored andcommitted
bpo-29406: asyncio SSL contexts leak sockets after calling close with certain servers (#409)
* asyncio SSL contexts leak sockets after calling close with certain servers * cleanup _shutdown_timeout_handle on _fatal_error
1 parent 34792d2 commit a608d2d

File tree

3 files changed

+62
-1
lines changed

3 files changed

+62
-1
lines changed

Lib/asyncio/sslproto.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
ssl = None
77

88
from . import base_events
9+
from . import compat
10+
from . import futures
911
from . import protocols
1012
from . import transports
1113
from .log import logger
@@ -407,7 +409,7 @@ class SSLProtocol(protocols.Protocol):
407409

408410
def __init__(self, loop, app_protocol, sslcontext, waiter,
409411
server_side=False, server_hostname=None,
410-
call_connection_made=True):
412+
call_connection_made=True, shutdown_timeout=5.0):
411413
if ssl is None:
412414
raise RuntimeError('stdlib ssl module not available')
413415

@@ -438,6 +440,8 @@ def __init__(self, loop, app_protocol, sslcontext, waiter,
438440
self._session_established = False
439441
self._in_handshake = False
440442
self._in_shutdown = False
443+
self._shutdown_timeout = shutdown_timeout
444+
self._shutdown_timeout_handle = None
441445
# transport, ex: SelectorSocketTransport
442446
self._transport = None
443447
self._call_connection_made = call_connection_made
@@ -552,6 +556,15 @@ def _start_shutdown(self):
552556
self._in_shutdown = True
553557
self._write_appdata(b'')
554558

559+
if self._shutdown_timeout is not None:
560+
self._shutdown_timeout_handle = self._loop.call_later(
561+
self._shutdown_timeout, self._on_shutdown_timeout)
562+
563+
def _on_shutdown_timeout(self):
564+
if self._transport is not None:
565+
self._fatal_error(
566+
futures.TimeoutError(), 'Can not complete shitdown operation')
567+
555568
def _write_appdata(self, data):
556569
self._write_backlog.append((data, 0))
557570
self._write_buffer_size += len(data)
@@ -679,12 +692,22 @@ def _fatal_error(self, exc, message='Fatal error on transport'):
679692
})
680693
if self._transport:
681694
self._transport._force_close(exc)
695+
self._transport = None
696+
697+
if self._shutdown_timeout_handle is not None:
698+
self._shutdown_timeout_handle.cancel()
699+
self._shutdown_timeout_handle = None
682700

683701
def _finalize(self):
684702
self._sslpipe = None
685703

686704
if self._transport is not None:
687705
self._transport.close()
706+
self._transport = None
707+
708+
if self._shutdown_timeout_handle is not None:
709+
self._shutdown_timeout_handle.cancel()
710+
self._shutdown_timeout_handle = None
688711

689712
def _abort(self):
690713
try:

Lib/test/test_asyncio/test_sslproto.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,40 @@ def test_connection_lost(self):
9696
test_utils.run_briefly(self.loop)
9797
self.assertIsInstance(waiter.exception(), ConnectionAbortedError)
9898

99+
def test_close_abort(self):
100+
# From issue #bpo-29406
101+
# abort connection if server does not complete shutdown procedure
102+
ssl_proto = self.ssl_protocol()
103+
transport = self.connection_made(ssl_proto)
104+
ssl_proto._on_handshake_complete(None)
105+
ssl_proto._start_shutdown()
106+
self.assertIsNotNone(ssl_proto._shutdown_timeout_handle)
107+
108+
exc_handler = mock.Mock()
109+
self.loop.set_exception_handler(exc_handler)
110+
ssl_proto._shutdown_timeout_handle._run()
111+
112+
exc_handler.assert_called_with(
113+
self.loop, {'message': 'Can not complete shitdown operation',
114+
'exception': mock.ANY,
115+
'transport': transport,
116+
'protocol': ssl_proto}
117+
)
118+
self.assertIsNone(ssl_proto._shutdown_timeout_handle)
119+
120+
def test_close(self):
121+
# From issue #bpo-29406
122+
# abort connection if server does not complete shutdown procedure
123+
ssl_proto = self.ssl_protocol()
124+
transport = self.connection_made(ssl_proto)
125+
ssl_proto._on_handshake_complete(None)
126+
ssl_proto._start_shutdown()
127+
self.assertIsNotNone(ssl_proto._shutdown_timeout_handle)
128+
129+
ssl_proto._finalize()
130+
self.assertIsNone(ssl_proto._transport)
131+
self.assertIsNone(ssl_proto._shutdown_timeout_handle)
132+
99133
def test_close_during_handshake(self):
100134
# bpo-29743 Closing transport during handshake process leaks socket
101135
waiter = asyncio.Future(loop=self.loop)

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,10 @@ Library
356356
- bpo-29743: Closing transport during handshake process leaks open socket.
357357
Patch by Nikolay Kim
358358

359+
- bpo-29406: asyncio SSL contexts leak sockets after calling close with
360+
certain servers.
361+
Patch by Nikolay Kim
362+
359363
- bpo-27585: Fix waiter cancellation in asyncio.Lock.
360364
Patch by Mathieu Sornay.
361365

0 commit comments

Comments
 (0)