Skip to content

Commit ec44b5a

Browse files
authored
[3.10] gh-156293: Use-after-free for server-side SSLContext with sni_callback (GH-158504) (#158524)
* [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. * [3.10] gh-156293: Document sni_callback dispatch after a context switch
1 parent dac88d8 commit ec44b5a

4 files changed

Lines changed: 126 additions & 12 deletions

File tree

‎Doc/library/ssl.rst‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1750,6 +1750,12 @@ to speed up repeated connections from the same clients.
17501750
:class:`SSLContext` representing a certificate chain that matches the server
17511751
name.
17521752

1753+
If the callback assigns a new context to :attr:`SSLSocket.context`, any
1754+
further ClientHello message on the same connection (for example after a
1755+
TLS 1.3 HelloRetryRequest) is dispatched to the new context's
1756+
*sni_callback*, if it has one; the original callback is not called again
1757+
for that connection.
1758+
17531759
Due to the early negotiation phase of the TLS connection, only limited
17541760
methods and attributes are usable like
17551761
:meth:`SSLSocket.selected_alpn_protocol` and :attr:`SSLSocket.context`.
@@ -1773,6 +1779,11 @@ to speed up repeated connections from the same clients.
17731779

17741780
.. versionadded:: 3.7
17751781

1782+
.. versionchanged:: next
1783+
After the callback assigns a new :attr:`SSLSocket.context`, later
1784+
ClientHello messages on the connection are dispatched to the new
1785+
context's *sni_callback*.
1786+
17761787
.. attribute:: SSLContext.set_servername_callback(server_name_callback)
17771788

17781789
This is a legacy API retained for backwards compatibility. When possible,

‎Lib/test/test_ssl.py‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2010,6 +2010,86 @@ def test_unwrap(self):
20102010
c_in.write(s_out.read())
20112011
client.unwrap()
20122012

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

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);
@@ -4390,27 +4393,37 @@ _ssl__SSLContext_set_ecdh_curve(PySSLContext *self, PyObject *name)
43904393
}
43914394

43924395
static int
4393-
_servername_callback(SSL *s, int *al, void *args)
4396+
_servername_callback(SSL *s, int *al, void *Py_UNUSED(args))
43944397
{
43954398
int ret;
4396-
PySSLContext *sslctx = (PySSLContext *) args;
4399+
PySSLContext *sslctx;
43974400
PySSLSocket *ssl;
43984401
PyObject *result;
43994402
/* The high-level ssl.SSLSocket object */
44004403
PyObject *ssl_socket;
4404+
PyObject *sni_cb;
44014405
const char *servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name);
44024406
PyGILState_STATE gstate = PyGILState_Ensure();
44034407

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

4411-
ssl = SSL_get_app_data(s);
4412-
assert(Py_IS_TYPE(ssl, get_state_ctx(sslctx)->PySSLSocket_Type));
4413-
44144427
/* The servername callback expects an argument that represents the current
44154428
* SSL connection and that has a .context attribute that can be changed to
44164429
* identify the requested hostname. Since the official API is the Python
@@ -4431,7 +4444,7 @@ _servername_callback(SSL *s, int *al, void *args)
44314444
goto error;
44324445

44334446
if (servername == NULL) {
4434-
result = PyObject_CallFunctionObjArgs(sslctx->set_sni_cb, ssl_socket,
4447+
result = PyObject_CallFunctionObjArgs(sni_cb, ssl_socket,
44354448
Py_None, sslctx, NULL);
44364449
}
44374450
else {
@@ -4454,14 +4467,14 @@ _servername_callback(SSL *s, int *al, void *args)
44544467
}
44554468
Py_DECREF(servername_bytes);
44564469
result = PyObject_CallFunctionObjArgs(
4457-
sslctx->set_sni_cb, ssl_socket, servername_str,
4470+
sni_cb, ssl_socket, servername_str,
44584471
sslctx, NULL);
44594472
Py_DECREF(servername_str);
44604473
}
44614474
Py_DECREF(ssl_socket);
44624475

44634476
if (result == NULL) {
4464-
PyErr_WriteUnraisable(sslctx->set_sni_cb);
4477+
PyErr_WriteUnraisable(sni_cb);
44654478
*al = SSL_AD_HANDSHAKE_FAILURE;
44664479
ret = SSL_TLSEXT_ERR_ALERT_FATAL;
44674480
}
@@ -4482,11 +4495,15 @@ _servername_callback(SSL *s, int *al, void *args)
44824495
Py_DECREF(result);
44834496
}
44844497

4498+
Py_DECREF(sni_cb);
4499+
Py_DECREF(sslctx);
44854500
PyGILState_Release(gstate);
44864501
return ret;
44874502

44884503
error:
44894504
Py_DECREF(ssl_socket);
4505+
Py_DECREF(sni_cb);
4506+
Py_DECREF(sslctx);
44904507
*al = SSL_AD_INTERNAL_ERROR;
44914508
ret = SSL_TLSEXT_ERR_ALERT_FATAL;
44924509
PyGILState_Release(gstate);
@@ -4526,7 +4543,6 @@ set_sni_callback(PySSLContext *self, PyObject *arg, void *c)
45264543
Py_INCREF(arg);
45274544
self->set_sni_cb = arg;
45284545
SSL_CTX_set_tlsext_servername_callback(self->ctx, _servername_callback);
4529-
SSL_CTX_set_tlsext_servername_arg(self->ctx, self);
45304546
}
45314547
return 0;
45324548
}

0 commit comments

Comments
 (0)