diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index ebdf5455163c65..e1ad23c4d4ea1f 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -44,6 +44,9 @@ from ssl import Purpose, TLSVersion, _TLSContentType, _TLSMessageType, _TLSAlertType Py_DEBUG_WIN32 = support.Py_DEBUG and sys.platform == 'win32' +HAS_KEYLOG = hasattr(ssl.SSLContext, 'keylog_filename') +requires_keylog = unittest.skipUnless( + HAS_KEYLOG, 'test requires OpenSSL 1.1.1 with keylog callback') PROTOCOLS = sorted(ssl._PROTOCOL_NAMES) HOST = socket_helper.HOST @@ -576,6 +579,53 @@ def test_refcycle(self): del ss self.assertEqual(wr(), None) + @support.cpython_only + def test_sslsocket_ctx_refcycle(self): + # SSLSocket doesn't leak when it has a reference cycle with its context + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.check_hostname = False + s = socket.socket(socket.AF_INET) + ss = ctx.wrap_socket(s) + # Create a cycle: ctx -> callback -> ss -> ctx + def msg_cb(conn, direction, version, content_type, msg_type, data): + pass + msg_cb.ss = ss + ctx._msg_callback = msg_cb + + ctx_wr = weakref.ref(ctx) + ss_wr = weakref.ref(ss) + ss.close() + del ctx, s, ss, msg_cb + gc.collect() + self.assertIs(ctx_wr(), None) + self.assertIs(ss_wr(), None) + + @support.cpython_only + def test_sslsocket_owner_refcycle(self): + # SSLSocket doesn't leak when it has a reference cycle with its owner + class Owner: + pass + owner = Owner() + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.check_hostname = False + s = socket.socket(socket.AF_INET) + # owner is only available in SSLObject.wrap_bio or _ssl._SSLSocket directly + # but SSLSocket doesn't expose owner in wrap_socket. + # We can use _sslobj.owner if we want to test the C-level leak. + ss = ctx.wrap_socket(s) + # SSLSocket._sslobj is None if wrap_socket failed or was not called correctly + # but here it should be a _ssl._SSLSocket + ss.owner = owner + owner.ss = ss + + owner_wr = weakref.ref(owner) + ss_wr = weakref.ref(ss) + ss.close() + del owner, ctx, s, ss + gc.collect() + self.assertIs(owner_wr(), None) + self.assertIs(ss_wr(), None) + def test_wrapped_unconnected(self): # Methods on an unconnected SSLSocket propagate the original # OSError raise by the underlying socket object. @@ -1488,6 +1538,49 @@ def dummycallback(sock, servername, ctx, cycle=ctx): gc.collect() self.assertIs(wr(), None) + @unittest.skipUnless(ssl.HAS_PSK, 'TLS-PSK disabled on this OpenSSL build') + def test_psk_client_callback_refcycle(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + def psk_cb(hint, cycle=ctx): + return (None, b"psk") + ctx.set_psk_client_callback(psk_cb) + wr = weakref.ref(ctx) + del ctx, psk_cb + gc.collect() + self.assertIs(wr(), None) + + @unittest.skipUnless(ssl.HAS_PSK, 'TLS-PSK disabled on this OpenSSL build') + def test_psk_server_callback_refcycle(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + def psk_cb(identity, cycle=ctx): + return b"psk" + ctx.set_psk_server_callback(psk_cb) + wr = weakref.ref(ctx) + del ctx, psk_cb + gc.collect() + self.assertIs(wr(), None) + + def test_msg_callback_refcycle(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + def msg_cb(conn, direction, version, content_type, msg_type, data, cycle=ctx): + pass + ctx._msg_callback = msg_cb + wr = weakref.ref(ctx) + del ctx, msg_cb + gc.collect() + self.assertIs(wr(), None) + + @requires_keylog + def test_keylog_filename_refcycle(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.keylog_filename = os_helper.TESTFN + # keylog_filename is a string, so it can't create a cycle itself, + # but we check that SSLContext still clears it. + ctx_wr = weakref.ref(ctx) + del ctx + gc.collect() + self.assertIs(ctx_wr(), None) + def test_cert_store_stats(self): ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) self.assertEqual(ctx.cert_store_stats(), @@ -4709,6 +4802,36 @@ def test_session_handling(self): self.assertEqual(str(e.exception), 'Session refers to a different SSLContext.') + @support.cpython_only + def test_session_refcycle(self): + # SSLSession doesn't leak when it has a reference cycle with its context + client_context, server_context, hostname = testing_context() + client_context.maximum_version = ssl.TLSVersion.TLSv1_2 + server = ThreadedEchoServer(context=server_context, chatty=False) + with server: + with client_context.wrap_socket(socket.socket(), + server_hostname=hostname) as s: + s.connect((HOST, server.port)) + session = s.session + + # Create a cycle: session -> ctx -> callback -> session + def msg_cb(conn, direction, version, content_type, msg_type, data): + pass + msg_cb.session = session + client_context._msg_callback = msg_cb + + # _ssl.SSLSession doesn't support weakrefs, so we use gc.get_referrers + # to check if it's still alive. + import gc + del session, client_context, server_context, s, msg_cb + gc.collect() + # If SSLSession is still alive, it should be in gc.get_objects() + # but that's a bit unreliable. Better check that there are no + # SSLSession objects left. + sessions = [obj for obj in gc.get_objects() + if type(obj).__name__ == 'SSLSession'] + self.assertEqual(sessions, []) + @requires_tls_version('TLSv1_2') @unittest.skipUnless(ssl.HAS_PSK, 'TLS-PSK disabled on this OpenSSL build') def test_psk(self): @@ -5163,9 +5286,6 @@ def test_internal_chain_server(self): self.assertEqual(res, b'\x02\n') -HAS_KEYLOG = hasattr(ssl.SSLContext, 'keylog_filename') -requires_keylog = unittest.skipUnless( - HAS_KEYLOG, 'test requires OpenSSL 1.1.1 with keylog callback') class TestSSLDebug(unittest.TestCase): @@ -5175,17 +5295,13 @@ def keylog_lines(self, fname=os_helper.TESTFN): @requires_keylog def test_keylog_defaults(self): + os_helper.unlink(os_helper.TESTFN) self.addCleanup(os_helper.unlink, os_helper.TESTFN) ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) self.assertEqual(ctx.keylog_filename, None) self.assertFalse(os.path.isfile(os_helper.TESTFN)) - try: - ctx.keylog_filename = os_helper.TESTFN - except RuntimeError: - if Py_DEBUG_WIN32: - self.skipTest("not supported on Win32 debug build") - raise + ctx.keylog_filename = os_helper.TESTFN self.assertEqual(ctx.keylog_filename, os_helper.TESTFN) self.assertTrue(os.path.isfile(os_helper.TESTFN)) self.assertEqual(self.keylog_lines(), 1) @@ -5206,12 +5322,7 @@ def test_keylog_filename(self): self.addCleanup(os_helper.unlink, os_helper.TESTFN) client_context, server_context, hostname = testing_context() - try: - client_context.keylog_filename = os_helper.TESTFN - except RuntimeError: - if Py_DEBUG_WIN32: - self.skipTest("not supported on Win32 debug build") - raise + client_context.keylog_filename = os_helper.TESTFN server = ThreadedEchoServer(context=server_context, chatty=False) with server: @@ -5254,12 +5365,7 @@ def test_keylog_env(self): ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) self.assertEqual(ctx.keylog_filename, None) - try: - ctx = ssl.create_default_context() - except RuntimeError: - if Py_DEBUG_WIN32: - self.skipTest("not supported on Win32 debug build") - raise + ctx = ssl.create_default_context() self.assertEqual(ctx.keylog_filename, os_helper.TESTFN) ctx = ssl._create_stdlib_context() diff --git a/Misc/NEWS.d/next/Library/2026-01-08-23-22-51.gh-issue-142516.0Sx7-Y.rst b/Misc/NEWS.d/next/Library/2026-01-08-23-22-51.gh-issue-142516.0Sx7-Y.rst new file mode 100644 index 00000000000000..4701a3e22a14f1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-08-23-22-51.gh-issue-142516.0Sx7-Y.rst @@ -0,0 +1 @@ +Fix memory leak in SSLContext diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 5d2f075ed0c675..af8537b5bc065d 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -316,6 +316,10 @@ enum py_proto_version { #define PySSL_CB_MAXLEN 128 +#if OPENSSL_VERSION_NUMBER >= 0x10101000L && !(defined(MS_WINDOWS) && defined(Py_DEBUG)) +# define PY_HAS_KEYLOG 1 +#endif + typedef struct { PyObject_HEAD SSL_CTX *ctx; @@ -328,8 +332,10 @@ typedef struct { int post_handshake_auth; #endif PyObject *msg_cb; +#ifdef PY_HAS_KEYLOG PyObject *keylog_filename; BIO *keylog_bio; +#endif /* Cached module state, also used in SSLSocket and SSLSession code. */ _sslmodulestate *state; #ifndef OPENSSL_NO_PSK @@ -355,7 +361,7 @@ typedef struct { PyObject_HEAD PyObject *Socket; /* weakref to socket on which we're layered */ SSL *ssl; - PySSLContext *ctx; /* weakref to SSL context */ + PySSLContext *ctx; /* SSL context */ char shutdown_seen_zero; enum py_ssl_server_or_client socket_type; PyObject *owner; /* Python level "owner" passed to servername callback */ @@ -2343,7 +2349,8 @@ static int _ssl__SSLSocket_context_set_impl(PySSLSocket *self, PyObject *value) /*[clinic end generated code: output=6b0a6cc5cf33d9fe input=f7fc1674b660df96]*/ { - if (PyObject_TypeCheck(value, self->ctx->state->PySSLContext_Type)) { + _sslmodulestate *state = get_state_obj(self); + if (PyObject_TypeCheck(value, state->PySSLContext_Type)) { Py_SETREF(self->ctx, (PySSLContext *)Py_NewRef(value)); SSL_set_SSL_CTX(self->ssl, self->ctx->ctx); /* Set SSL* internal msg_callback to state of new context's state */ @@ -2435,6 +2442,10 @@ static int PySSL_traverse(PyObject *op, visitproc visit, void *arg) { PySSLSocket *self = PySSLSocket_CAST(op); + Py_VISIT(self->Socket); + Py_VISIT(self->ctx); + Py_VISIT(self->server_hostname); + Py_VISIT(self->owner); Py_VISIT(self->exc); Py_VISIT(Py_TYPE(self)); return 0; @@ -2444,6 +2455,10 @@ static int PySSL_clear(PyObject *op) { PySSLSocket *self = PySSLSocket_CAST(op); + Py_CLEAR(self->Socket); + Py_CLEAR(self->ctx); + Py_CLEAR(self->server_hostname); + Py_CLEAR(self->owner); Py_CLEAR(self->exc); return 0; } @@ -2468,10 +2483,7 @@ PySSL_dealloc(PyObject *op) SSL_set_shutdown(self->ssl, SSL_SENT_SHUTDOWN | SSL_get_shutdown(self->ssl)); SSL_free(self->ssl); } - Py_XDECREF(self->Socket); - Py_XDECREF(self->ctx); - Py_XDECREF(self->server_hostname); - Py_XDECREF(self->owner); + (void)PySSL_clear(op); PyObject_GC_Del(self); Py_DECREF(tp); } @@ -3482,8 +3494,10 @@ _ssl__SSLContext_impl(PyTypeObject *type, int proto_version) self->ctx = ctx; self->protocol = proto_version; self->msg_cb = NULL; +#ifdef PY_HAS_KEYLOG self->keylog_filename = NULL; self->keylog_bio = NULL; +#endif self->alpn_protocols = NULL; self->set_sni_cb = NULL; self->state = get_ssl_state(module); @@ -3594,6 +3608,13 @@ context_traverse(PyObject *op, visitproc visit, void *arg) PySSLContext *self = PySSLContext_CAST(op); Py_VISIT(self->set_sni_cb); Py_VISIT(self->msg_cb); +#ifdef PY_HAS_KEYLOG + Py_VISIT(self->keylog_filename); +#endif +#ifndef OPENSSL_NO_PSK + Py_VISIT(self->psk_client_callback); + Py_VISIT(self->psk_server_callback); +#endif Py_VISIT(Py_TYPE(self)); return 0; } @@ -3604,11 +3625,14 @@ context_clear(PyObject *op) PySSLContext *self = PySSLContext_CAST(op); Py_CLEAR(self->set_sni_cb); Py_CLEAR(self->msg_cb); +#ifdef PY_HAS_KEYLOG Py_CLEAR(self->keylog_filename); +#endif #ifndef OPENSSL_NO_PSK Py_CLEAR(self->psk_client_callback); Py_CLEAR(self->psk_server_callback); #endif +#ifdef PY_HAS_KEYLOG if (self->keylog_bio != NULL) { Py_BEGIN_ALLOW_THREADS BIO_free_all(self->keylog_bio); @@ -3616,6 +3640,7 @@ context_clear(PyObject *op) _PySSL_FIX_ERRNO; self->keylog_bio = NULL; } +#endif return 0; } @@ -5670,8 +5695,10 @@ static PyGetSetDef context_getsetlist[] = { _SSL__SSLCONTEXT__HOST_FLAGS_GETSETDEF _SSL__SSLCONTEXT_MINIMUM_VERSION_GETSETDEF _SSL__SSLCONTEXT_MAXIMUM_VERSION_GETSETDEF +#ifdef PY_HAS_KEYLOG {"keylog_filename", _PySSLContext_get_keylog_filename, _PySSLContext_set_keylog_filename, NULL}, +#endif {"_msg_callback", _PySSLContext_get_msg_callback, _PySSLContext_set_msg_callback, NULL}, _SSL__SSLCONTEXT_SNI_CALLBACK_GETSETDEF @@ -5959,6 +5986,23 @@ static PyType_Spec PySSLMemoryBIO_spec = { * SSL Session object */ +static int +PySSLSession_traverse(PyObject *op, visitproc visit, void *arg) +{ + PySSLSession *self = PySSLSession_CAST(op); + Py_VISIT(self->ctx); + Py_VISIT(Py_TYPE(self)); + return 0; +} + +static int +PySSLSession_clear(PyObject *op) +{ + PySSLSession *self = PySSLSession_CAST(op); + Py_CLEAR(self->ctx); + return 0; +} + static void PySSLSession_dealloc(PyObject *op) { @@ -5966,10 +6010,10 @@ PySSLSession_dealloc(PyObject *op) PyTypeObject *tp = Py_TYPE(self); /* bpo-31095: UnTrack is needed before calling any callbacks */ PyObject_GC_UnTrack(self); - Py_XDECREF(self->ctx); if (self->session != NULL) { SSL_SESSION_free(self->session); } + (void)PySSLSession_clear(op); PyObject_GC_Del(self); Py_DECREF(tp); } @@ -5982,8 +6026,23 @@ PySSLSession_richcompare(PyObject *left, PyObject *right, int op) return NULL; } + PySSLSession *left_sess = PySSLSession_CAST(left); + PySSLSession *right_sess = PySSLSession_CAST(right); + + if (left_sess->ctx == NULL || right_sess->ctx == NULL) { + if (op == Py_EQ) { + if (left == right) Py_RETURN_TRUE; + Py_RETURN_FALSE; + } + if (op == Py_NE) { + if (left != right) Py_RETURN_TRUE; + Py_RETURN_FALSE; + } + Py_RETURN_NOTIMPLEMENTED; + } + int result; - PyTypeObject *sesstype = PySSLSession_CAST(left)->ctx->state->PySSLSession_Type; + PyTypeObject *sesstype = left_sess->ctx->state->PySSLSession_Type; if (!Py_IS_TYPE(left, sesstype) || !Py_IS_TYPE(right, sesstype)) { Py_RETURN_NOTIMPLEMENTED; @@ -6032,24 +6091,6 @@ PySSLSession_richcompare(PyObject *left, PyObject *right, int op) } } -static int -PySSLSession_traverse(PyObject *op, visitproc visit, void *arg) -{ - PySSLSession *self = PySSLSession_CAST(op); - Py_VISIT(self->ctx); - Py_VISIT(Py_TYPE(self)); - return 0; -} - -static int -PySSLSession_clear(PyObject *op) -{ - PySSLSession *self = PySSLSession_CAST(op); - Py_CLEAR(self->ctx); - return 0; -} - - /*[clinic input] @critical_section @getter diff --git a/Modules/_ssl/debughelpers.c b/Modules/_ssl/debughelpers.c index 866c172e4996f7..f4ec23bed282b2 100644 --- a/Modules/_ssl/debughelpers.c +++ b/Modules/_ssl/debughelpers.c @@ -117,6 +117,7 @@ _PySSLContext_set_msg_callback(PyObject *op, PyObject *arg, return 0; } +#ifdef PY_HAS_KEYLOG static void _PySSL_keylog_callback(const SSL *ssl, const char *line) { @@ -178,12 +179,6 @@ _PySSLContext_set_keylog_filename(PyObject *op, PyObject *arg, PySSLContext *self = PySSLContext_CAST(op); FILE *fp; -#if defined(MS_WINDOWS) && defined(Py_DEBUG) - PyErr_SetString(PyExc_NotImplementedError, - "set_keylog_filename: unavailable on Windows debug build"); - return -1; -#endif - /* Reset variables and callback first */ SSL_CTX_set_keylog_callback(self->ctx, NULL); Py_CLEAR(self->keylog_filename); @@ -225,3 +220,4 @@ _PySSLContext_set_keylog_filename(PyObject *op, PyObject *arg, SSL_CTX_set_keylog_callback(self->ctx, _PySSL_keylog_callback); return 0; } +#endif