Skip to content

Commit 6e14fd2

Browse files
authored
[3.6] bpo-29406: asyncio SSL contexts leak sockets after calling close with certain servers (GH-409) (#2062)
* bpo-29406: asyncio SSL contexts leak sockets after calling close with certain servers (#409) (cherry picked from commit a608d2d) * [3.6] bpo-29406: asyncio SSL contexts leak sockets after calling close with certain servers (GH-409) * asyncio SSL contexts leak sockets after calling close with certain servers * cleanup _shutdown_timeout_handle on _fatal_error. (cherry picked from commit a608d2d)
1 parent 911068e commit 6e14fd2

File tree

3 files changed

+61
-1
lines changed

3 files changed

+61
-1
lines changed

Lib/asyncio/sslproto.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from . import base_events
99
from . import compat
10+
from . import futures
1011
from . import protocols
1112
from . import transports
1213
from .log import logger
@@ -412,7 +413,7 @@ class SSLProtocol(protocols.Protocol):
412413

413414
def __init__(self, loop, app_protocol, sslcontext, waiter,
414415
server_side=False, server_hostname=None,
415-
call_connection_made=True):
416+
call_connection_made=True, shutdown_timeout=5.0):
416417
if ssl is None:
417418
raise RuntimeError('stdlib ssl module not available')
418419

@@ -443,6 +444,8 @@ def __init__(self, loop, app_protocol, sslcontext, waiter,
443444
self._session_established = False
444445
self._in_handshake = False
445446
self._in_shutdown = False
447+
self._shutdown_timeout = shutdown_timeout
448+
self._shutdown_timeout_handle = None
446449
# transport, ex: SelectorSocketTransport
447450
self._transport = None
448451
self._call_connection_made = call_connection_made
@@ -557,6 +560,15 @@ def _start_shutdown(self):
557560
self._in_shutdown = True
558561
self._write_appdata(b'')
559562

563+
if self._shutdown_timeout is not None:
564+
self._shutdown_timeout_handle = self._loop.call_later(
565+
self._shutdown_timeout, self._on_shutdown_timeout)
566+
567+
def _on_shutdown_timeout(self):
568+
if self._transport is not None:
569+
self._fatal_error(
570+
futures.TimeoutError(), 'Can not complete shitdown operation')
571+
560572
def _write_appdata(self, data):
561573
self._write_backlog.append((data, 0))
562574
self._write_buffer_size += len(data)
@@ -684,12 +696,22 @@ def _fatal_error(self, exc, message='Fatal error on transport'):
684696
})
685697
if self._transport:
686698
self._transport._force_close(exc)
699+
self._transport = None
700+
701+
if self._shutdown_timeout_handle is not None:
702+
self._shutdown_timeout_handle.cancel()
703+
self._shutdown_timeout_handle = None
687704

688705
def _finalize(self):
689706
self._sslpipe = None
690707

691708
if self._transport is not None:
692709
self._transport.close()
710+
self._transport = None
711+
712+
if self._shutdown_timeout_handle is not None:
713+
self._shutdown_timeout_handle.cancel()
714+
self._shutdown_timeout_handle = None
693715

694716
def _abort(self):
695717
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
@@ -49,6 +49,10 @@ Core and Builtins
4949
Library
5050
-------
5151

52+
- bpo-29406: asyncio SSL contexts leak sockets after calling close with
53+
certain servers.
54+
Patch by Nikolay Kim
55+
5256
- bpo-29870: Fix ssl sockets leaks when connection is aborted in asyncio/ssl
5357
implementation. Patch by Michaël Sghaïer.
5458

0 commit comments

Comments
 (0)