From 8cc1861ad8446bae1fee507acf652efa6ffb926d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:22:27 +0100 Subject: [PATCH 1/2] gh-146080: fix a crash in SNI callbacks when the SSL object is gone --- Lib/test/test_ssl.py | 50 +++++++++++++++++++ ...-03-28-13-19-20.gh-issue-146080.srN12a.rst | 2 + Modules/_ssl.c | 2 +- 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-03-28-13-19-20.gh-issue-146080.srN12a.rst diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 61355927296474..8ba0016772d111 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -1,5 +1,6 @@ # Test the support for SSL and sockets +import contextlib import sys import unittest import unittest.mock @@ -383,6 +384,20 @@ def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True, return client_context, server_context, hostname +def do_ssl_object_handshake(sslobject, outgoing, max_retry=25): + """Call do_handshake() on the sslobject and return the sent data. + + If do_handshake() fails more than *max_retry* times, return None. + """ + data, attempt = None, 0 + while not data and attempt < max_retry: + with contextlib.suppress(ssl.SSLWantReadError): + sslobject.do_handshake() + data = outgoing.read() + attempt += 1 + return data + + class BasicSocketTests(unittest.TestCase): def test_constants(self): @@ -1535,6 +1550,41 @@ def dummycallback(sock, servername, ctx): ctx.set_servername_callback(None) ctx.set_servername_callback(dummycallback) + def test_sni_callback_on_dead_references(self): + # See https://github.com/python/cpython/issues/146080. + c_ctx = make_test_context() + c_inc, c_out = ssl.MemoryBIO(), ssl.MemoryBIO() + client = c_ctx.wrap_bio(c_inc, c_out, server_hostname=SIGNED_CERTFILE_HOSTNAME) + + def sni_callback(sock, servername, ctx): pass + sni_callback = unittest.mock.Mock(wraps=sni_callback) + s_ctx = make_test_context(server_side=True, certfile=SIGNED_CERTFILE) + s_ctx.set_servername_callback(sni_callback) + + s_inc, s_out = ssl.MemoryBIO(), ssl.MemoryBIO() + server = s_ctx.wrap_bio(s_inc, s_out, server_side=True) + server_impl = server._sslobj + + # Perform the handshake on the client side first. + data = do_ssl_object_handshake(client, c_out) + sni_callback.assert_not_called() + if data is None: + self.skipTest("cannot establish a handshake from the client") + s_inc.write(data) + sni_callback.assert_not_called() + # Delete the server object before it starts doing its handshake + # and ensure that we did not call the SNI callback yet. + del server + gc.collect() + # Try to continue the server's handshake by directly using + # the internal SSL object. The latter is a weak reference + # stored in the server context and has now a dead owner. + with self.assertRaisesRegex(ssl.SSLError, "callback failed") as cm: + server_impl.do_handshake() + # The SNI C callback raised an exception before calling our callback. + sni_callback.assert_not_called() + self.assertEqual(cm.exception.errno, ssl.SSL_ERROR_SSL) + def test_sni_callback_refcycle(self): # Reference cycles through the servername callback are detected # and cleared. diff --git a/Misc/NEWS.d/next/Library/2026-03-28-13-19-20.gh-issue-146080.srN12a.rst b/Misc/NEWS.d/next/Library/2026-03-28-13-19-20.gh-issue-146080.srN12a.rst new file mode 100644 index 00000000000000..c80e8e05d480e5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-28-13-19-20.gh-issue-146080.srN12a.rst @@ -0,0 +1,2 @@ +:mod:`ssl`: fix a crash when an SNI callback tries to use an SSL object that +has already been garbage-collected. Patch by Bénédikt Tran. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 6f75af861135d6..d42a4e7f7078e6 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -5205,7 +5205,7 @@ _servername_callback(SSL *s, int *al, void *args) return ret; error: - Py_DECREF(ssl_socket); + Py_XDECREF(ssl_socket); *al = SSL_AD_INTERNAL_ERROR; ret = SSL_TLSEXT_ERR_ALERT_FATAL; PyGILState_Release(gstate); From d8911ce0478082b40bf72b64cc41e422903c3572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:41:20 +0100 Subject: [PATCH 2/2] fix AWS-LC compatibility --- Lib/test/test_ssl.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 8ba0016772d111..cd90e332443800 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -48,12 +48,13 @@ PROTOCOLS = sorted(ssl._PROTOCOL_NAMES) HOST = socket_helper.HOST +IS_AWS_LC = "AWS-LC" in ssl.OPENSSL_VERSION IS_OPENSSL_3_0_0 = ssl.OPENSSL_VERSION_INFO >= (3, 0, 0) CAN_GET_SELECTED_OPENSSL_GROUP = ssl.OPENSSL_VERSION_INFO >= (3, 2) CAN_IGNORE_UNKNOWN_OPENSSL_GROUPS = ssl.OPENSSL_VERSION_INFO >= (3, 3) CAN_GET_AVAILABLE_OPENSSL_GROUPS = ssl.OPENSSL_VERSION_INFO >= (3, 5) CAN_GET_AVAILABLE_OPENSSL_SIGALGS = ssl.OPENSSL_VERSION_INFO >= (3, 4) -CAN_SET_CLIENT_SIGALGS = "AWS-LC" not in ssl.OPENSSL_VERSION +CAN_SET_CLIENT_SIGALGS = not IS_AWS_LC CAN_IGNORE_UNKNOWN_OPENSSL_SIGALGS = ssl.OPENSSL_VERSION_INFO >= (3, 3) CAN_GET_SELECTED_OPENSSL_SIGALG = ssl.OPENSSL_VERSION_INFO >= (3, 5) PY_SSL_DEFAULT_CIPHERS = sysconfig.get_config_var('PY_SSL_DEFAULT_CIPHERS') @@ -1579,10 +1580,18 @@ def sni_callback(sock, servername, ctx): pass # Try to continue the server's handshake by directly using # the internal SSL object. The latter is a weak reference # stored in the server context and has now a dead owner. - with self.assertRaisesRegex(ssl.SSLError, "callback failed") as cm: + with self.assertRaises(ssl.SSLError) as cm: server_impl.do_handshake() # The SNI C callback raised an exception before calling our callback. sni_callback.assert_not_called() + + # In AWS-LC, any handshake failures reports SSL_R_PARSE_TLSEXT, + # while OpenSSL uses SSL_R_CALLBACK_FAILED on SNI callback failures. + if IS_AWS_LC: + libssl_error_reason = "PARSE_TLSEXT" + else: + libssl_error_reason = "callback failed" + self.assertIn(libssl_error_reason, str(cm.exception)) self.assertEqual(cm.exception.errno, ssl.SSL_ERROR_SSL) def test_sni_callback_refcycle(self):