Skip to content

Commit 7ab129e

Browse files
committed
[3.10] 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 1071290 commit 7ab129e

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
@@ -2008,6 +2008,86 @@ def test_unwrap(self):
20082008
c_in.write(s_out.read())
20092009
client.unwrap()
20102010

2011+
def test_sni_callback_context_released_and_callback_raises(self):
2012+
# Variant of the test below without a HelloRetryRequest: the callback
2013+
# switches the connection to another context, drops the last
2014+
# references to the context that carries it, and raises. The C
2015+
# callback must not touch that context after the Python callback
2016+
# returned.
2017+
client_ctx, server_ctx, hostname = testing_context()
2018+
leaf_ctx = server_ctx
2019+
2020+
def sni_cb(sslobj, server_name, ctx):
2021+
sslobj.context = leaf_ctx
2022+
del ctx
2023+
raise LookupError("no certificate for " + repr(server_name))
2024+
2025+
def make_server():
2026+
dispatch_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
2027+
dispatch_ctx.load_cert_chain(SIGNED_CERTFILE)
2028+
dispatch_ctx.sni_callback = sni_cb
2029+
s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO()
2030+
server = dispatch_ctx.wrap_bio(s_in, s_out, server_side=True)
2031+
return server, s_in, s_out
2032+
2033+
server, s_in, s_out = make_server()
2034+
c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO()
2035+
client = client_ctx.wrap_bio(c_in, c_out, server_hostname=hostname)
2036+
with self.assertRaises(ssl.SSLWantReadError):
2037+
client.do_handshake()
2038+
s_in.write(c_out.read())
2039+
with support.catch_unraisable_exception() as cm:
2040+
with self.assertRaises(ssl.SSLError):
2041+
server.do_handshake()
2042+
self.assertIsInstance(cm.unraisable.exc_value, LookupError)
2043+
self.assertIs(server.context, leaf_ctx)
2044+
2045+
def test_sni_callback_context_released_before_second_client_hello(self):
2046+
# The SSLContext carrying sni_callback may be released by the
2047+
# application once the callback has switched the connection over to
2048+
# another context. If the server then sends a HelloRetryRequest, the
2049+
# second ClientHello makes OpenSSL consult the original SSL_CTX's
2050+
# servername callback again; that must not use the deallocated
2051+
# SSLContext object.
2052+
client_ctx, leaf_ctx, hostname = testing_context()
2053+
calls = []
2054+
2055+
def make_server():
2056+
dispatch_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
2057+
dispatch_ctx.load_cert_chain(SIGNED_CERTFILE)
2058+
# Force a HelloRetryRequest: the client offers an X25519 key
2059+
# share first, the server only accepts P-384.
2060+
dispatch_ctx.set_ecdh_curve("secp384r1")
2061+
def sni_cb(sslobj, server_name, ctx):
2062+
calls.append(server_name)
2063+
sslobj.context = leaf_ctx
2064+
dispatch_ctx.sni_callback = sni_cb
2065+
s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO()
2066+
server = dispatch_ctx.wrap_bio(s_in, s_out, server_side=True)
2067+
return server, s_in, s_out, weakref.ref(dispatch_ctx)
2068+
2069+
# After this only the C-level SSL object references dispatch_ctx.
2070+
server, s_in, s_out, dispatch_ref = make_server()
2071+
c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO()
2072+
client = client_ctx.wrap_bio(c_in, c_out, server_hostname=hostname)
2073+
for _ in range(10):
2074+
for obj, out, peer_in in ((client, c_out, s_in),
2075+
(server, s_out, c_in)):
2076+
try:
2077+
obj.do_handshake()
2078+
except ssl.SSLWantReadError:
2079+
pass
2080+
if out.pending:
2081+
peer_in.write(out.read())
2082+
client.do_handshake()
2083+
server.do_handshake()
2084+
support.gc_collect()
2085+
self.assertIsNone(dispatch_ref())
2086+
self.assertGreaterEqual(len(calls), 1)
2087+
self.assertEqual(calls[0], hostname)
2088+
self.assertIs(server.context, leaf_ctx)
2089+
self.assertIsNotNone(client.cipher())
2090+
20112091
class SimpleBackgroundTests(unittest.TestCase):
20122092
"""Tests that connect to a simple server running in the background"""
20132093

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
@@ -3252,6 +3252,9 @@ context_dealloc(PySSLContext *self)
32523252
/* bpo-31095: UnTrack is needed before calling any callbacks */
32533253
PyObject_GC_UnTrack(self);
32543254
context_clear(self);
3255+
/* The SSL_CTX may outlive this object as the session_ctx of sockets that
3256+
were switched to another context; leave no Python callback behind. */
3257+
SSL_CTX_set_tlsext_servername_callback(self->ctx, NULL);
32553258
SSL_CTX_free(self->ctx);
32563259
PyMem_FREE(self->alpn_protocols);
32573260
Py_TYPE(self)->tp_free(self);
@@ -4388,27 +4391,37 @@ _ssl__SSLContext_set_ecdh_curve(PySSLContext *self, PyObject *name)
43884391
}
43894392

43904393
static int
4391-
_servername_callback(SSL *s, int *al, void *args)
4394+
_servername_callback(SSL *s, int *al, void *Py_UNUSED(args))
43924395
{
43934396
int ret;
4394-
PySSLContext *sslctx = (PySSLContext *) args;
4397+
PySSLContext *sslctx;
43954398
PySSLSocket *ssl;
43964399
PyObject *result;
43974400
/* The high-level ssl.SSLSocket object */
43984401
PyObject *ssl_socket;
4402+
PyObject *sni_cb;
43994403
const char *servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name);
44004404
PyGILState_STATE gstate = PyGILState_Ensure();
44014405

4402-
if (sslctx->set_sni_cb == NULL) {
4403-
/* remove race condition in this the call back while if removing the
4404-
* callback is in progress */
4406+
/* Do not use the SSL_CTX's servername arg to find the context: it is a
4407+
borrowed pointer to whichever _SSLContext installed the callback, and
4408+
that object may already be gone while OpenSSL still reaches this
4409+
callback through the connection's session_ctx (e.g. on the second
4410+
ClientHello after a HelloRetryRequest, once sni_callback has switched
4411+
the socket to another context). The socket's current context is
4412+
always alive; hold strong references to it and to the callback while
4413+
they are used here. */
4414+
ssl = SSL_get_app_data(s);
4415+
assert(ssl != NULL);
4416+
sslctx = (PySSLContext *)Py_NewRef(ssl->ctx);
4417+
assert(Py_IS_TYPE(ssl, get_state_ctx(sslctx)->PySSLSocket_Type));
4418+
sni_cb = Py_XNewRef(sslctx->set_sni_cb);
4419+
if (sni_cb == NULL) {
4420+
Py_DECREF(sslctx);
44054421
PyGILState_Release(gstate);
44064422
return SSL_TLSEXT_ERR_OK;
44074423
}
44084424

4409-
ssl = SSL_get_app_data(s);
4410-
assert(Py_IS_TYPE(ssl, get_state_ctx(sslctx)->PySSLSocket_Type));
4411-
44124425
/* The servername callback expects an argument that represents the current
44134426
* SSL connection and that has a .context attribute that can be changed to
44144427
* identify the requested hostname. Since the official API is the Python
@@ -4429,7 +4442,7 @@ _servername_callback(SSL *s, int *al, void *args)
44294442
goto error;
44304443

44314444
if (servername == NULL) {
4432-
result = PyObject_CallFunctionObjArgs(sslctx->set_sni_cb, ssl_socket,
4445+
result = PyObject_CallFunctionObjArgs(sni_cb, ssl_socket,
44334446
Py_None, sslctx, NULL);
44344447
}
44354448
else {
@@ -4452,14 +4465,14 @@ _servername_callback(SSL *s, int *al, void *args)
44524465
}
44534466
Py_DECREF(servername_bytes);
44544467
result = PyObject_CallFunctionObjArgs(
4455-
sslctx->set_sni_cb, ssl_socket, servername_str,
4468+
sni_cb, ssl_socket, servername_str,
44564469
sslctx, NULL);
44574470
Py_DECREF(servername_str);
44584471
}
44594472
Py_DECREF(ssl_socket);
44604473

44614474
if (result == NULL) {
4462-
PyErr_WriteUnraisable(sslctx->set_sni_cb);
4475+
PyErr_WriteUnraisable(sni_cb);
44634476
*al = SSL_AD_HANDSHAKE_FAILURE;
44644477
ret = SSL_TLSEXT_ERR_ALERT_FATAL;
44654478
}
@@ -4480,11 +4493,15 @@ _servername_callback(SSL *s, int *al, void *args)
44804493
Py_DECREF(result);
44814494
}
44824495

4496+
Py_DECREF(sni_cb);
4497+
Py_DECREF(sslctx);
44834498
PyGILState_Release(gstate);
44844499
return ret;
44854500

44864501
error:
44874502
Py_DECREF(ssl_socket);
4503+
Py_DECREF(sni_cb);
4504+
Py_DECREF(sslctx);
44884505
*al = SSL_AD_INTERNAL_ERROR;
44894506
ret = SSL_TLSEXT_ERR_ALERT_FATAL;
44904507
PyGILState_Release(gstate);
@@ -4524,7 +4541,6 @@ set_sni_callback(PySSLContext *self, PyObject *arg, void *c)
45244541
Py_INCREF(arg);
45254542
self->set_sni_cb = arg;
45264543
SSL_CTX_set_tlsext_servername_callback(self->ctx, _servername_callback);
4527-
SSL_CTX_set_tlsext_servername_arg(self->ctx, self);
45284544
}
45294545
return 0;
45304546
}

0 commit comments

Comments
 (0)