@@ -576,6 +576,53 @@ def test_refcycle(self):
576576 del ss
577577 self .assertEqual (wr (), None )
578578
579+ @support .cpython_only
580+ def test_sslsocket_ctx_refcycle (self ):
581+ # SSLSocket doesn't leak when it has a reference cycle with its context
582+ ctx = ssl .SSLContext (ssl .PROTOCOL_TLS_CLIENT )
583+ ctx .check_hostname = False
584+ s = socket .socket (socket .AF_INET )
585+ ss = ctx .wrap_socket (s )
586+ # Create a cycle: ctx -> callback -> ss -> ctx
587+ def msg_cb (conn , direction , version , content_type , msg_type , data ):
588+ pass
589+ msg_cb .ss = ss
590+ ctx ._msg_callback = msg_cb
591+
592+ ctx_wr = weakref .ref (ctx )
593+ ss_wr = weakref .ref (ss )
594+ ss .close ()
595+ del ctx , s , ss , msg_cb
596+ gc .collect ()
597+ self .assertIs (ctx_wr (), None )
598+ self .assertIs (ss_wr (), None )
599+
600+ @support .cpython_only
601+ def test_sslsocket_owner_refcycle (self ):
602+ # SSLSocket doesn't leak when it has a reference cycle with its owner
603+ class Owner :
604+ pass
605+ owner = Owner ()
606+ ctx = ssl .SSLContext (ssl .PROTOCOL_TLS_CLIENT )
607+ ctx .check_hostname = False
608+ s = socket .socket (socket .AF_INET )
609+ # owner is only available in SSLObject.wrap_bio or _ssl._SSLSocket directly
610+ # but SSLSocket doesn't expose owner in wrap_socket.
611+ # We can use _sslobj.owner if we want to test the C-level leak.
612+ ss = ctx .wrap_socket (s )
613+ # SSLSocket._sslobj is None if wrap_socket failed or was not called correctly
614+ # but here it should be a _ssl._SSLSocket
615+ ss .owner = owner
616+ owner .ss = ss
617+
618+ owner_wr = weakref .ref (owner )
619+ ss_wr = weakref .ref (ss )
620+ ss .close ()
621+ del owner , ctx , s , ss
622+ gc .collect ()
623+ self .assertIs (owner_wr (), None )
624+ self .assertIs (ss_wr (), None )
625+
579626 def test_wrapped_unconnected (self ):
580627 # Methods on an unconnected SSLSocket propagate the original
581628 # OSError raise by the underlying socket object.
@@ -1488,6 +1535,48 @@ def dummycallback(sock, servername, ctx, cycle=ctx):
14881535 gc .collect ()
14891536 self .assertIs (wr (), None )
14901537
1538+ @unittest .skipUnless (ssl .HAS_PSK , 'TLS-PSK disabled on this OpenSSL build' )
1539+ def test_psk_client_callback_refcycle (self ):
1540+ ctx = ssl .SSLContext (ssl .PROTOCOL_TLS_CLIENT )
1541+ def psk_cb (hint , cycle = ctx ):
1542+ return (None , b"psk" )
1543+ ctx .set_psk_client_callback (psk_cb )
1544+ wr = weakref .ref (ctx )
1545+ del ctx , psk_cb
1546+ gc .collect ()
1547+ self .assertIs (wr (), None )
1548+
1549+ @unittest .skipUnless (ssl .HAS_PSK , 'TLS-PSK disabled on this OpenSSL build' )
1550+ def test_psk_server_callback_refcycle (self ):
1551+ ctx = ssl .SSLContext (ssl .PROTOCOL_TLS_SERVER )
1552+ def psk_cb (identity , cycle = ctx ):
1553+ return b"psk"
1554+ ctx .set_psk_server_callback (psk_cb )
1555+ wr = weakref .ref (ctx )
1556+ del ctx , psk_cb
1557+ gc .collect ()
1558+ self .assertIs (wr (), None )
1559+
1560+ def test_msg_callback_refcycle (self ):
1561+ ctx = ssl .SSLContext (ssl .PROTOCOL_TLS_CLIENT )
1562+ def msg_cb (conn , direction , version , content_type , msg_type , data , cycle = ctx ):
1563+ pass
1564+ ctx ._msg_callback = msg_cb
1565+ wr = weakref .ref (ctx )
1566+ del ctx , msg_cb
1567+ gc .collect ()
1568+ self .assertIs (wr (), None )
1569+
1570+ def test_keylog_filename_refcycle (self ):
1571+ ctx = ssl .SSLContext (ssl .PROTOCOL_TLS_CLIENT )
1572+ ctx .keylog_filename = os_helper .TESTFN
1573+ # keylog_filename is a string, so it can't create a cycle itself,
1574+ # but we check that SSLContext still clears it.
1575+ ctx_wr = weakref .ref (ctx )
1576+ del ctx
1577+ gc .collect ()
1578+ self .assertIs (ctx_wr (), None )
1579+
14911580 def test_cert_store_stats (self ):
14921581 ctx = ssl .SSLContext (ssl .PROTOCOL_TLS_CLIENT )
14931582 self .assertEqual (ctx .cert_store_stats (),
@@ -4709,6 +4798,36 @@ def test_session_handling(self):
47094798 self .assertEqual (str (e .exception ),
47104799 'Session refers to a different SSLContext.' )
47114800
4801+ @support .cpython_only
4802+ def test_session_refcycle (self ):
4803+ # SSLSession doesn't leak when it has a reference cycle with its context
4804+ client_context , server_context , hostname = testing_context ()
4805+ client_context .maximum_version = ssl .TLSVersion .TLSv1_2
4806+ server = ThreadedEchoServer (context = server_context , chatty = False )
4807+ with server :
4808+ with client_context .wrap_socket (socket .socket (),
4809+ server_hostname = hostname ) as s :
4810+ s .connect ((HOST , server .port ))
4811+ session = s .session
4812+
4813+ # Create a cycle: session -> ctx -> callback -> session
4814+ def msg_cb (conn , direction , version , content_type , msg_type , data ):
4815+ pass
4816+ msg_cb .session = session
4817+ client_context ._msg_callback = msg_cb
4818+
4819+ # _ssl.SSLSession doesn't support weakrefs, so we use gc.get_referrers
4820+ # to check if it's still alive.
4821+ import gc
4822+ del session , client_context , server_context , s , msg_cb
4823+ gc .collect ()
4824+ # If SSLSession is still alive, it should be in gc.get_objects()
4825+ # but that's a bit unreliable. Better check that there are no
4826+ # SSLSession objects left.
4827+ sessions = [obj for obj in gc .get_objects ()
4828+ if type (obj ).__name__ == 'SSLSession' ]
4829+ self .assertEqual (sessions , [])
4830+
47124831 @requires_tls_version ('TLSv1_2' )
47134832 @unittest .skipUnless (ssl .HAS_PSK , 'TLS-PSK disabled on this OpenSSL build' )
47144833 def test_psk (self ):
0 commit comments