Skip to content

Commit b12968c

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

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
@@ -1748,6 +1748,12 @@ to speed up repeated connections from the same clients.
17481748
:class:`SSLContext` representing a certificate chain that matches the server
17491749
name.
17501750

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

17721778
.. versionadded:: 3.7
17731779

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

17761787
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
@@ -2040,6 +2040,86 @@ def test_unwrap(self):
20402040
c_in.write(s_out.read())
20412041
client.unwrap()
20422042

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

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);
@@ -4412,27 +4415,37 @@ _ssl__SSLContext_set_ecdh_curve(PySSLContext *self, PyObject *name)
44124415
}
44134416

44144417
static int
4415-
_servername_callback(SSL *s, int *al, void *args)
4418+
_servername_callback(SSL *s, int *al, void *Py_UNUSED(args))
44164419
{
44174420
int ret;
4418-
PySSLContext *sslctx = (PySSLContext *) args;
4421+
PySSLContext *sslctx;
44194422
PySSLSocket *ssl;
44204423
PyObject *result;
44214424
/* The high-level ssl.SSLSocket object */
44224425
PyObject *ssl_socket;
4426+
PyObject *sni_cb;
44234427
const char *servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name);
44244428
PyGILState_STATE gstate = PyGILState_Ensure();
44254429

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

4433-
ssl = SSL_get_app_data(s);
4434-
assert(Py_IS_TYPE(ssl, get_state_ctx(sslctx)->PySSLSocket_Type));
4435-
44364449
/* The servername callback expects an argument that represents the current
44374450
* SSL connection and that has a .context attribute that can be changed to
44384451
* identify the requested hostname. Since the official API is the Python
@@ -4453,7 +4466,7 @@ _servername_callback(SSL *s, int *al, void *args)
44534466
goto error;
44544467

44554468
if (servername == NULL) {
4456-
result = PyObject_CallFunctionObjArgs(sslctx->set_sni_cb, ssl_socket,
4469+
result = PyObject_CallFunctionObjArgs(sni_cb, ssl_socket,
44574470
Py_None, sslctx, NULL);
44584471
}
44594472
else {
@@ -4476,14 +4489,14 @@ _servername_callback(SSL *s, int *al, void *args)
44764489
}
44774490
Py_DECREF(servername_bytes);
44784491
result = PyObject_CallFunctionObjArgs(
4479-
sslctx->set_sni_cb, ssl_socket, servername_str,
4492+
sni_cb, ssl_socket, servername_str,
44804493
sslctx, NULL);
44814494
Py_DECREF(servername_str);
44824495
}
44834496
Py_DECREF(ssl_socket);
44844497

44854498
if (result == NULL) {
4486-
PyErr_WriteUnraisable(sslctx->set_sni_cb);
4499+
PyErr_WriteUnraisable(sni_cb);
44874500
*al = SSL_AD_HANDSHAKE_FAILURE;
44884501
ret = SSL_TLSEXT_ERR_ALERT_FATAL;
44894502
}
@@ -4504,11 +4517,15 @@ _servername_callback(SSL *s, int *al, void *args)
45044517
Py_DECREF(result);
45054518
}
45064519

4520+
Py_DECREF(sni_cb);
4521+
Py_DECREF(sslctx);
45074522
PyGILState_Release(gstate);
45084523
return ret;
45094524

45104525
error:
45114526
Py_DECREF(ssl_socket);
4527+
Py_DECREF(sni_cb);
4528+
Py_DECREF(sslctx);
45124529
*al = SSL_AD_INTERNAL_ERROR;
45134530
ret = SSL_TLSEXT_ERR_ALERT_FATAL;
45144531
PyGILState_Release(gstate);
@@ -4548,7 +4565,6 @@ set_sni_callback(PySSLContext *self, PyObject *arg, void *c)
45484565
Py_INCREF(arg);
45494566
self->set_sni_cb = arg;
45504567
SSL_CTX_set_tlsext_servername_callback(self->ctx, _servername_callback);
4551-
SSL_CTX_set_tlsext_servername_arg(self->ctx, self);
45524568
}
45534569
return 0;
45544570
}

0 commit comments

Comments
 (0)