diff --git a/tests/test_tcp.py b/tests/test_tcp.py index 459ef307..dcf497f6 100644 --- a/tests/test_tcp.py +++ b/tests/test_tcp.py @@ -2651,6 +2651,73 @@ async def test(): else: self.fail('Unexpected ResourceWarning: {}'.format(cm.warning)) + def test_handshake_timeout_handler_leak(self): + if self.implementation == 'asyncio': + # Okay this turns out to be an issue for asyncio.sslproto too + raise unittest.SkipTest() + + s = socket.socket(socket.AF_INET) + s.bind(('127.0.0.1', 0)) + s.listen(1) + addr = s.getsockname() + + async def test(ctx): + try: + await asyncio.wait_for( + self.loop.create_connection(asyncio.Protocol, *addr, + ssl=ctx), + 0.1, loop=self.loop) + except (ConnectionRefusedError, asyncio.TimeoutError): + pass + else: + self.fail('TimeoutError is not raised') + + with s: + ctx = ssl.create_default_context() + self.loop.run_until_complete(test(ctx)) + ctx = weakref.ref(ctx) + + # SSLProtocol should be DECREF to 0 + self.assertIsNone(ctx()) + + def test_shutdown_timeout_handler_leak(self): + loop = self.loop + + def server(sock): + sslctx = self._create_server_ssl_context(self.ONLYCERT, + self.ONLYKEY) + sock = sslctx.wrap_socket(sock, server_side=True) + sock.recv(32) + sock.close() + + class Protocol(asyncio.Protocol): + def __init__(self): + self.fut = asyncio.Future(loop=loop) + + def connection_lost(self, exc): + self.fut.set_result(None) + + async def client(addr, ctx): + tr, pr = await loop.create_connection(Protocol, *addr, ssl=ctx) + tr.close() + await pr.fut + + with self.tcp_server(server) as srv: + ctx = self._create_client_ssl_context() + loop.run_until_complete(client(srv.addr, ctx)) + ctx = weakref.ref(ctx) + + if self.implementation == 'asyncio': + # asyncio has no shutdown timeout, but it ends up with a circular + # reference loop - not ideal (introduces gc glitches), but at least + # not leaking + gc.collect() + gc.collect() + gc.collect() + + # SSLProtocol should be DECREF to 0 + self.assertIsNone(ctx()) + class Test_UV_TCPSSL(_TestSSL, tb.UVTestCase): pass diff --git a/uvloop/sslproto.pyx b/uvloop/sslproto.pyx index c9f57ba6..3f342fa0 100644 --- a/uvloop/sslproto.pyx +++ b/uvloop/sslproto.pyx @@ -341,9 +341,9 @@ cdef class SSLProtocol: self._app_transport = None self._wakeup_waiter(exc) - if getattr(self, '_shutdown_timeout_handle', None): + if self._shutdown_timeout_handle: self._shutdown_timeout_handle.cancel() - if getattr(self, '_handshake_timeout_handle', None): + if self._handshake_timeout_handle: self._handshake_timeout_handle.cancel() def get_buffer(self, n): @@ -453,8 +453,13 @@ cdef class SSLProtocol: self._set_state(DO_HANDSHAKE) # start handshake timeout count down + # XXX: This is a workaround to call cdef method and clear reference to + # self when the handle is cancelled (because uvloop currently clears + # only args, it'll be okay to remove the lambda x: x() after the fix to + # clear also the callback) self._handshake_timeout_handle = \ self._loop.call_later(self._ssl_handshake_timeout, + lambda x: x(), lambda: self._check_handshake_timeout()) try: @@ -533,8 +538,13 @@ cdef class SSLProtocol: self._abort(None) else: self._set_state(FLUSHING) + # XXX: This is a workaround to call cdef method and clear reference + # to self when the handle is cancelled (because uvloop currently + # clears only args, it'll be okay to remove the lambda x: x() after + # the fix to clear also the callback) self._shutdown_timeout_handle = \ self._loop.call_later(self._ssl_shutdown_timeout, + lambda x: x(), lambda: self._check_shutdown_timeout()) self._do_flush()