Skip to content

Commit c0827e6

Browse files
committed
Possible fix for #142516
1 parent e2f15ae commit c0827e6

File tree

2 files changed

+151
-23
lines changed

2 files changed

+151
-23
lines changed

Lib/test/test_ssl.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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):

Modules/_ssl.c

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2435,6 +2435,10 @@ static int
24352435
PySSL_traverse(PyObject *op, visitproc visit, void *arg)
24362436
{
24372437
PySSLSocket *self = PySSLSocket_CAST(op);
2438+
Py_VISIT(self->Socket);
2439+
Py_VISIT(self->ctx);
2440+
Py_VISIT(self->server_hostname);
2441+
Py_VISIT(self->owner);
24382442
Py_VISIT(self->exc);
24392443
Py_VISIT(Py_TYPE(self));
24402444
return 0;
@@ -2444,6 +2448,10 @@ static int
24442448
PySSL_clear(PyObject *op)
24452449
{
24462450
PySSLSocket *self = PySSLSocket_CAST(op);
2451+
Py_CLEAR(self->Socket);
2452+
Py_CLEAR(self->ctx);
2453+
Py_CLEAR(self->server_hostname);
2454+
Py_CLEAR(self->owner);
24472455
Py_CLEAR(self->exc);
24482456
return 0;
24492457
}
@@ -2468,10 +2476,7 @@ PySSL_dealloc(PyObject *op)
24682476
SSL_set_shutdown(self->ssl, SSL_SENT_SHUTDOWN | SSL_get_shutdown(self->ssl));
24692477
SSL_free(self->ssl);
24702478
}
2471-
Py_XDECREF(self->Socket);
2472-
Py_XDECREF(self->ctx);
2473-
Py_XDECREF(self->server_hostname);
2474-
Py_XDECREF(self->owner);
2479+
(void)PySSL_clear(op);
24752480
PyObject_GC_Del(self);
24762481
Py_DECREF(tp);
24772482
}
@@ -3594,6 +3599,11 @@ context_traverse(PyObject *op, visitproc visit, void *arg)
35943599
PySSLContext *self = PySSLContext_CAST(op);
35953600
Py_VISIT(self->set_sni_cb);
35963601
Py_VISIT(self->msg_cb);
3602+
Py_VISIT(self->keylog_filename);
3603+
#ifndef OPENSSL_NO_PSK
3604+
Py_VISIT(self->psk_client_callback);
3605+
Py_VISIT(self->psk_server_callback);
3606+
#endif
35973607
Py_VISIT(Py_TYPE(self));
35983608
return 0;
35993609
}
@@ -5959,17 +5969,34 @@ static PyType_Spec PySSLMemoryBIO_spec = {
59595969
* SSL Session object
59605970
*/
59615971

5972+
static int
5973+
PySSLSession_traverse(PyObject *op, visitproc visit, void *arg)
5974+
{
5975+
PySSLSession *self = PySSLSession_CAST(op);
5976+
Py_VISIT(self->ctx);
5977+
Py_VISIT(Py_TYPE(self));
5978+
return 0;
5979+
}
5980+
5981+
static int
5982+
PySSLSession_clear(PyObject *op)
5983+
{
5984+
PySSLSession *self = PySSLSession_CAST(op);
5985+
Py_CLEAR(self->ctx);
5986+
return 0;
5987+
}
5988+
59625989
static void
59635990
PySSLSession_dealloc(PyObject *op)
59645991
{
59655992
PySSLSession *self = PySSLSession_CAST(op);
59665993
PyTypeObject *tp = Py_TYPE(self);
59675994
/* bpo-31095: UnTrack is needed before calling any callbacks */
59685995
PyObject_GC_UnTrack(self);
5969-
Py_XDECREF(self->ctx);
59705996
if (self->session != NULL) {
59715997
SSL_SESSION_free(self->session);
59725998
}
5999+
(void)PySSLSession_clear(op);
59736000
PyObject_GC_Del(self);
59746001
Py_DECREF(tp);
59756002
}
@@ -6032,24 +6059,6 @@ PySSLSession_richcompare(PyObject *left, PyObject *right, int op)
60326059
}
60336060
}
60346061

6035-
static int
6036-
PySSLSession_traverse(PyObject *op, visitproc visit, void *arg)
6037-
{
6038-
PySSLSession *self = PySSLSession_CAST(op);
6039-
Py_VISIT(self->ctx);
6040-
Py_VISIT(Py_TYPE(self));
6041-
return 0;
6042-
}
6043-
6044-
static int
6045-
PySSLSession_clear(PyObject *op)
6046-
{
6047-
PySSLSession *self = PySSLSession_CAST(op);
6048-
Py_CLEAR(self->ctx);
6049-
return 0;
6050-
}
6051-
6052-
60536062
/*[clinic input]
60546063
@critical_section
60556064
@getter

0 commit comments

Comments
 (0)