Skip to content

Commit 64d5656

Browse files
committed
[3.11] gh-156293: ssl: do not call the servername callback through a released SSLContext
The servername (SNI) callback located its SSLContext through a borrowed pointer registered with OpenSSL, which can outlive the SSLContext object. Look the context up from the SSL object instead, and unregister the callback when the context is deallocated. Backport of 87665c9, adapted to this branch's servername callback code.
1 parent 32eb314 commit 64d5656

3 files changed

Lines changed: 115 additions & 12 deletions

File tree

‎Lib/test/test_ssl.py‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2038,6 +2038,86 @@ def test_unwrap(self):
20382038
c_in.write(s_out.read())
20392039
client.unwrap()
20402040

2041+
def test_sni_callback_context_released_and_callback_raises(self):
2042+
# Variant of the test below without a HelloRetryRequest: the callback
2043+
# switches the connection to another context, drops the last
2044+
# references to the context that carries it, and raises. The C
2045+
# callback must not touch that context after the Python callback
2046+
# returned.
2047+
client_ctx, server_ctx, hostname = testing_context()
2048+
leaf_ctx = server_ctx
2049+
2050+
def sni_cb(sslobj, server_name, ctx):
2051+
sslobj.context = leaf_ctx
2052+
del ctx
2053+
raise LookupError("no certificate for " + repr(server_name))
2054+
2055+
def make_server():
2056+
dispatch_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
2057+
dispatch_ctx.load_cert_chain(SIGNED_CERTFILE)
2058+
dispatch_ctx.sni_callback = sni_cb
2059+
s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO()
2060+
server = dispatch_ctx.wrap_bio(s_in, s_out, server_side=True)
2061+
return server, s_in, s_out
2062+
2063+
server, s_in, s_out = make_server()
2064+
c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO()
2065+
client = client_ctx.wrap_bio(c_in, c_out, server_hostname=hostname)
2066+
with self.assertRaises(ssl.SSLWantReadError):
2067+
client.do_handshake()
2068+
s_in.write(c_out.read())
2069+
with support.catch_unraisable_exception() as cm:
2070+
with self.assertRaises(ssl.SSLError):
2071+
server.do_handshake()
2072+
self.assertIsInstance(cm.unraisable.exc_value, LookupError)
2073+
self.assertIs(server.context, leaf_ctx)
2074+
2075+
def test_sni_callback_context_released_before_second_client_hello(self):
2076+
# The SSLContext carrying sni_callback may be released by the
2077+
# application once the callback has switched the connection over to
2078+
# another context. If the server then sends a HelloRetryRequest, the
2079+
# second ClientHello makes OpenSSL consult the original SSL_CTX's
2080+
# servername callback again; that must not use the deallocated
2081+
# SSLContext object.
2082+
client_ctx, leaf_ctx, hostname = testing_context()
2083+
calls = []
2084+
2085+
def make_server():
2086+
dispatch_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
2087+
dispatch_ctx.load_cert_chain(SIGNED_CERTFILE)
2088+
# Force a HelloRetryRequest: the client offers an X25519 key
2089+
# share first, the server only accepts P-384.
2090+
dispatch_ctx.set_ecdh_curve("secp384r1")
2091+
def sni_cb(sslobj, server_name, ctx):
2092+
calls.append(server_name)
2093+
sslobj.context = leaf_ctx
2094+
dispatch_ctx.sni_callback = sni_cb
2095+
s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO()
2096+
server = dispatch_ctx.wrap_bio(s_in, s_out, server_side=True)
2097+
return server, s_in, s_out, weakref.ref(dispatch_ctx)
2098+
2099+
# After this only the C-level SSL object references dispatch_ctx.
2100+
server, s_in, s_out, dispatch_ref = make_server()
2101+
c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO()
2102+
client = client_ctx.wrap_bio(c_in, c_out, server_hostname=hostname)
2103+
for _ in range(10):
2104+
for obj, out, peer_in in ((client, c_out, s_in),
2105+
(server, s_out, c_in)):
2106+
try:
2107+
obj.do_handshake()
2108+
except ssl.SSLWantReadError:
2109+
pass
2110+
if out.pending:
2111+
peer_in.write(out.read())
2112+
client.do_handshake()
2113+
server.do_handshake()
2114+
support.gc_collect()
2115+
self.assertIsNone(dispatch_ref())
2116+
self.assertGreaterEqual(len(calls), 1)
2117+
self.assertEqual(calls[0], hostname)
2118+
self.assertIs(server.context, leaf_ctx)
2119+
self.assertIsNotNone(client.cipher())
2120+
20412121
class SimpleBackgroundTests(unittest.TestCase):
20422122
"""Tests that connect to a simple server running in the background"""
20432123

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Fix a crash in :mod:`ssl` when an :attr:`~ssl.SSLContext.sni_callback`
2+
switches a connection to another :class:`~ssl.SSLContext` and the context
3+
that carries the callback is no longer referenced by the application.
4+
Servers that keep their ``sni_callback`` context alive (the usual case when
5+
it wraps the listening socket or is stored on the server object) were not
6+
affected.
7+
This addresses `CVE-2026-19445 <https://www.cve.org/CVERecord?id=CVE-2026-19445>`_.

‎Modules/_ssl.c‎

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3256,6 +3256,9 @@ context_dealloc(PySSLContext *self)
32563256
/* bpo-31095: UnTrack is needed before calling any callbacks */
32573257
PyObject_GC_UnTrack(self);
32583258
context_clear(self);
3259+
/* The SSL_CTX may outlive this object as the session_ctx of sockets that
3260+
were switched to another context; leave no Python callback behind. */
3261+
SSL_CTX_set_tlsext_servername_callback(self->ctx, NULL);
32593262
SSL_CTX_free(self->ctx);
32603263
PyMem_FREE(self->alpn_protocols);
32613264
Py_TYPE(self)->tp_free(self);
@@ -4410,27 +4413,37 @@ _ssl__SSLContext_set_ecdh_curve(PySSLContext *self, PyObject *name)
44104413
}
44114414

44124415
static int
4413-
_servername_callback(SSL *s, int *al, void *args)
4416+
_servername_callback(SSL *s, int *al, void *Py_UNUSED(args))
44144417
{
44154418
int ret;
4416-
PySSLContext *sslctx = (PySSLContext *) args;
4419+
PySSLContext *sslctx;
44174420
PySSLSocket *ssl;
44184421
PyObject *result;
44194422
/* The high-level ssl.SSLSocket object */
44204423
PyObject *ssl_socket;
4424+
PyObject *sni_cb;
44214425
const char *servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name);
44224426
PyGILState_STATE gstate = PyGILState_Ensure();
44234427

4424-
if (sslctx->set_sni_cb == NULL) {
4425-
/* remove race condition in this the call back while if removing the
4426-
* callback is in progress */
4428+
/* Do not use the SSL_CTX's servername arg to find the context: it is a
4429+
borrowed pointer to whichever _SSLContext installed the callback, and
4430+
that object may already be gone while OpenSSL still reaches this
4431+
callback through the connection's session_ctx (e.g. on the second
4432+
ClientHello after a HelloRetryRequest, once sni_callback has switched
4433+
the socket to another context). The socket's current context is
4434+
always alive; hold strong references to it and to the callback while
4435+
they are used here. */
4436+
ssl = SSL_get_app_data(s);
4437+
assert(ssl != NULL);
4438+
sslctx = (PySSLContext *)Py_NewRef(ssl->ctx);
4439+
assert(Py_IS_TYPE(ssl, get_state_ctx(sslctx)->PySSLSocket_Type));
4440+
sni_cb = Py_XNewRef(sslctx->set_sni_cb);
4441+
if (sni_cb == NULL) {
4442+
Py_DECREF(sslctx);
44274443
PyGILState_Release(gstate);
44284444
return SSL_TLSEXT_ERR_OK;
44294445
}
44304446

4431-
ssl = SSL_get_app_data(s);
4432-
assert(Py_IS_TYPE(ssl, get_state_ctx(sslctx)->PySSLSocket_Type));
4433-
44344447
/* The servername callback expects an argument that represents the current
44354448
* SSL connection and that has a .context attribute that can be changed to
44364449
* identify the requested hostname. Since the official API is the Python
@@ -4451,7 +4464,7 @@ _servername_callback(SSL *s, int *al, void *args)
44514464
goto error;
44524465

44534466
if (servername == NULL) {
4454-
result = PyObject_CallFunctionObjArgs(sslctx->set_sni_cb, ssl_socket,
4467+
result = PyObject_CallFunctionObjArgs(sni_cb, ssl_socket,
44554468
Py_None, sslctx, NULL);
44564469
}
44574470
else {
@@ -4474,14 +4487,14 @@ _servername_callback(SSL *s, int *al, void *args)
44744487
}
44754488
Py_DECREF(servername_bytes);
44764489
result = PyObject_CallFunctionObjArgs(
4477-
sslctx->set_sni_cb, ssl_socket, servername_str,
4490+
sni_cb, ssl_socket, servername_str,
44784491
sslctx, NULL);
44794492
Py_DECREF(servername_str);
44804493
}
44814494
Py_DECREF(ssl_socket);
44824495

44834496
if (result == NULL) {
4484-
PyErr_WriteUnraisable(sslctx->set_sni_cb);
4497+
PyErr_WriteUnraisable(sni_cb);
44854498
*al = SSL_AD_HANDSHAKE_FAILURE;
44864499
ret = SSL_TLSEXT_ERR_ALERT_FATAL;
44874500
}
@@ -4502,11 +4515,15 @@ _servername_callback(SSL *s, int *al, void *args)
45024515
Py_DECREF(result);
45034516
}
45044517

4518+
Py_DECREF(sni_cb);
4519+
Py_DECREF(sslctx);
45054520
PyGILState_Release(gstate);
45064521
return ret;
45074522

45084523
error:
45094524
Py_DECREF(ssl_socket);
4525+
Py_DECREF(sni_cb);
4526+
Py_DECREF(sslctx);
45104527
*al = SSL_AD_INTERNAL_ERROR;
45114528
ret = SSL_TLSEXT_ERR_ALERT_FATAL;
45124529
PyGILState_Release(gstate);
@@ -4546,7 +4563,6 @@ set_sni_callback(PySSLContext *self, PyObject *arg, void *c)
45464563
Py_INCREF(arg);
45474564
self->set_sni_cb = arg;
45484565
SSL_CTX_set_tlsext_servername_callback(self->ctx, _servername_callback);
4549-
SSL_CTX_set_tlsext_servername_arg(self->ctx, self);
45504566
}
45514567
return 0;
45524568
}

0 commit comments

Comments
 (0)