Skip to content

Commit 1395c58

Browse files
authored
[3.5] bpo-29406: asyncio SSL contexts leak sockets after calling close with certain servers (GH-409) (#2063)
* bpo-29406: asyncio SSL contexts leak sockets after calling close with certain servers (#409) (cherry picked from commit a608d2d) * [3.5] 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 1f73023 commit 1395c58

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
@@ -411,7 +412,7 @@ class SSLProtocol(protocols.Protocol):
411412

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

@@ -442,6 +443,8 @@ def __init__(self, loop, app_protocol, sslcontext, waiter,
442443
self._session_established = False
443444
self._in_handshake = False
444445
self._in_shutdown = False
446+
self._shutdown_timeout = shutdown_timeout
447+
self._shutdown_timeout_handle = None
445448
# transport, ex: SelectorSocketTransport
446449
self._transport = None
447450
self._call_connection_made = call_connection_made
@@ -556,6 +559,15 @@ def _start_shutdown(self):
556559
self._in_shutdown = True
557560
self._write_appdata(b'')
558561

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

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

690707
if self._transport is not None:
691708
self._transport.close()
709+
self._transport = None
710+
711+
if self._shutdown_timeout_handle is not None:
712+
self._shutdown_timeout_handle.cancel()
713+
self._shutdown_timeout_handle = None
692714

693715
def _abort(self):
694716
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
@@ -62,6 +62,10 @@ Library
6262
- bpo-29743: Closing transport during handshake process leaks open socket.
6363
Patch by Nikolay Kim
6464

65+
- bpo-29406: asyncio SSL contexts leak sockets after calling close with
66+
certain servers.
67+
Patch by Nikolay Kim
68+
6569
- bpo-27585: Fix waiter cancellation in asyncio.Lock.
6670
Patch by Mathieu Sornay.
6771

0 commit comments

Comments
 (0)