https://github.com/python/cpython/commit/24db78c5329dd405460bfdf76df380ced6231353
commit: 24db78c5329dd405460bfdf76df380ced6231353
branch: main
author: Bénédikt Tran <[email protected]>
committer: picnixz <[email protected]>
date: 2026-03-29T14:27:22+02:00
summary:
gh-146080: fix a crash in SNI callbacks when the SSL object is gone (#146573)
files:
A Misc/NEWS.d/next/Library/2026-03-28-13-19-20.gh-issue-146080.srN12a.rst
M Lib/test/test_ssl.py
M Modules/_ssl.c
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
index 61355927296474..cd90e332443800 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
@@ -47,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')
@@ -383,6 +385,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 +1551,49 @@ 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.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):
# 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);
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]