Skip to content

Commit d8717ed

Browse files
sethmlarsongpshead
andauthored
[3.13] gh-156293: Use-after-free for server-side SSLContext with sni_… (#158507)
Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
1 parent 25335f9 commit d8717ed

4 files changed

Lines changed: 132 additions & 13 deletions

File tree

‎Doc/library/ssl.rst‎

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

1717+
If the callback assigns a new context to :attr:`SSLSocket.context`, any
1718+
further ClientHello message on the same connection (for example after a
1719+
TLS 1.3 HelloRetryRequest) is dispatched to the new context's
1720+
*sni_callback*, if it has one; the original callback is not called again
1721+
for that connection.
1722+
17171723
Due to the early negotiation phase of the TLS connection, only limited
17181724
methods and attributes are usable like
17191725
:meth:`SSLSocket.selected_alpn_protocol` and :attr:`SSLSocket.context`.
@@ -1738,6 +1744,11 @@ to speed up repeated connections from the same clients.
17381744

17391745
.. versionadded:: 3.7
17401746

1747+
.. versionchanged:: next
1748+
After the callback assigns a new :attr:`SSLSocket.context`, later
1749+
ClientHello messages on the connection are dispatched to the new
1750+
context's *sni_callback*.
1751+
17411752
.. method:: SSLContext.set_servername_callback(server_name_callback)
17421753

17431754
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
@@ -2067,6 +2067,86 @@ def test_unwrap(self):
20672067
c_in.write(s_out.read())
20682068
client.unwrap()
20692069

2070+
def test_sni_callback_context_released_and_callback_raises(self):
2071+
# Variant of the test below without a HelloRetryRequest: the callback
2072+
# switches the connection to another context, drops the last
2073+
# references to the context that carries it, and raises. The C
2074+
# callback must not touch that context after the Python callback
2075+
# returned.
2076+
client_ctx, server_ctx, hostname = testing_context()
2077+
leaf_ctx = server_ctx
2078+
2079+
def sni_cb(sslobj, server_name, ctx):
2080+
sslobj.context = leaf_ctx
2081+
del ctx
2082+
raise LookupError("no certificate for " + repr(server_name))
2083+
2084+
def make_server():
2085+
dispatch_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
2086+
dispatch_ctx.load_cert_chain(SIGNED_CERTFILE)
2087+
dispatch_ctx.sni_callback = sni_cb
2088+
s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO()
2089+
server = dispatch_ctx.wrap_bio(s_in, s_out, server_side=True)
2090+
return server, s_in, s_out
2091+
2092+
server, s_in, s_out = make_server()
2093+
c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO()
2094+
client = client_ctx.wrap_bio(c_in, c_out, server_hostname=hostname)
2095+
with self.assertRaises(ssl.SSLWantReadError):
2096+
client.do_handshake()
2097+
s_in.write(c_out.read())
2098+
with support.catch_unraisable_exception() as cm:
2099+
with self.assertRaises(ssl.SSLError):
2100+
server.do_handshake()
2101+
self.assertIsInstance(cm.unraisable.exc_value, LookupError)
2102+
self.assertIs(server.context, leaf_ctx)
2103+
2104+
def test_sni_callback_context_released_before_second_client_hello(self):
2105+
# The SSLContext carrying sni_callback may be released by the
2106+
# application once the callback has switched the connection over to
2107+
# another context. If the server then sends a HelloRetryRequest, the
2108+
# second ClientHello makes OpenSSL consult the original SSL_CTX's
2109+
# servername callback again; that must not use the deallocated
2110+
# SSLContext object.
2111+
client_ctx, leaf_ctx, hostname = testing_context()
2112+
calls = []
2113+
2114+
def make_server():
2115+
dispatch_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
2116+
dispatch_ctx.load_cert_chain(SIGNED_CERTFILE)
2117+
# Force a HelloRetryRequest: the client offers an X25519 key
2118+
# share first, the server only accepts P-384.
2119+
dispatch_ctx.set_ecdh_curve("secp384r1")
2120+
def sni_cb(sslobj, server_name, ctx):
2121+
calls.append(server_name)
2122+
sslobj.context = leaf_ctx
2123+
dispatch_ctx.sni_callback = sni_cb
2124+
s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO()
2125+
server = dispatch_ctx.wrap_bio(s_in, s_out, server_side=True)
2126+
return server, s_in, s_out, weakref.ref(dispatch_ctx)
2127+
2128+
# After this only the C-level SSL object references dispatch_ctx.
2129+
server, s_in, s_out, dispatch_ref = make_server()
2130+
c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO()
2131+
client = client_ctx.wrap_bio(c_in, c_out, server_hostname=hostname)
2132+
for _ in range(10):
2133+
for obj, out, peer_in in ((client, c_out, s_in),
2134+
(server, s_out, c_in)):
2135+
try:
2136+
obj.do_handshake()
2137+
except ssl.SSLWantReadError:
2138+
pass
2139+
if out.pending:
2140+
peer_in.write(out.read())
2141+
client.do_handshake()
2142+
server.do_handshake()
2143+
support.gc_collect()
2144+
self.assertIsNone(dispatch_ref())
2145+
self.assertGreaterEqual(len(calls), 1)
2146+
self.assertEqual(calls[0], hostname)
2147+
self.assertIs(server.context, leaf_ctx)
2148+
self.assertIsNotNone(client.cipher())
2149+
20702150
class SimpleBackgroundTests(unittest.TestCase):
20712151
"""Tests that connect to a simple server running in the background"""
20722152

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`.

‎Modules/_ssl.c‎

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3296,6 +3296,9 @@ context_dealloc(PySSLContext *self)
32963296
/* bpo-31095: UnTrack is needed before calling any callbacks */
32973297
PyObject_GC_UnTrack(self);
32983298
context_clear(self);
3299+
/* The SSL_CTX may outlive this object as the session_ctx of sockets that
3300+
were switched to another context; leave no Python callback behind. */
3301+
SSL_CTX_set_tlsext_servername_callback(self->ctx, NULL);
32993302
SSL_CTX_free(self->ctx);
33003303
PyMem_FREE(self->alpn_protocols);
33013304
Py_TYPE(self)->tp_free(self);
@@ -4606,27 +4609,42 @@ _ssl__SSLContext_set_ecdh_curve_impl(PySSLContext *self, PyObject *name)
46064609
}
46074610

46084611
static int
4609-
_servername_callback(SSL *s, int *al, void *args)
4612+
_servername_callback(SSL *s, int *al, void *Py_UNUSED(args))
46104613
{
46114614
int ret;
4612-
PySSLContext *sslctx = (PySSLContext *) args;
4615+
PySSLContext *sslctx;
46134616
PySSLSocket *ssl;
46144617
PyObject *result;
46154618
/* The high-level ssl.SSLSocket object */
4616-
PyObject *ssl_socket;
4619+
PyObject *ssl_socket = NULL;
4620+
PyObject *sni_cb;
46174621
const char *servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name);
46184622
PyGILState_STATE gstate = PyGILState_Ensure();
46194623

4620-
if (sslctx->set_sni_cb == NULL) {
4621-
/* remove race condition in this the call back while if removing the
4622-
* callback is in progress */
4624+
/* Do not use the SSL_CTX's servername arg to find the context: it is a
4625+
borrowed pointer to whichever _SSLContext installed the callback, and
4626+
that object may already be gone while OpenSSL still reaches this
4627+
callback through the connection's session_ctx (e.g. on the second
4628+
ClientHello after a HelloRetryRequest, once sni_callback has switched
4629+
the socket to another context). The socket's current context is
4630+
always alive; hold strong references to it and to the callback while
4631+
they are used here. */
4632+
ssl = SSL_get_app_data(s);
4633+
assert(ssl != NULL);
4634+
Py_BEGIN_CRITICAL_SECTION(ssl);
4635+
sslctx = (PySSLContext *)Py_NewRef(ssl->ctx);
4636+
Py_END_CRITICAL_SECTION();
4637+
assert(Py_IS_TYPE(ssl, get_state_ctx(sslctx)->PySSLSocket_Type));
4638+
4639+
Py_BEGIN_CRITICAL_SECTION(sslctx);
4640+
sni_cb = Py_XNewRef(sslctx->set_sni_cb);
4641+
Py_END_CRITICAL_SECTION();
4642+
if (sni_cb == NULL) {
4643+
Py_DECREF(sslctx);
46234644
PyGILState_Release(gstate);
46244645
return SSL_TLSEXT_ERR_OK;
46254646
}
46264647

4627-
ssl = SSL_get_app_data(s);
4628-
assert(Py_IS_TYPE(ssl, get_state_ctx(sslctx)->PySSLSocket_Type));
4629-
46304648
/* The servername callback expects an argument that represents the current
46314649
* SSL connection and that has a .context attribute that can be changed to
46324650
* identify the requested hostname. Since the official API is the Python
@@ -4646,7 +4664,7 @@ _servername_callback(SSL *s, int *al, void *args)
46464664
goto error;
46474665

46484666
if (servername == NULL) {
4649-
result = PyObject_CallFunctionObjArgs(sslctx->set_sni_cb, ssl_socket,
4667+
result = PyObject_CallFunctionObjArgs(sni_cb, ssl_socket,
46504668
Py_None, sslctx, NULL);
46514669
}
46524670
else {
@@ -4669,14 +4687,14 @@ _servername_callback(SSL *s, int *al, void *args)
46694687
}
46704688
Py_DECREF(servername_bytes);
46714689
result = PyObject_CallFunctionObjArgs(
4672-
sslctx->set_sni_cb, ssl_socket, servername_str,
4690+
sni_cb, ssl_socket, servername_str,
46734691
sslctx, NULL);
46744692
Py_DECREF(servername_str);
46754693
}
46764694
Py_DECREF(ssl_socket);
46774695

46784696
if (result == NULL) {
4679-
PyErr_WriteUnraisable(sslctx->set_sni_cb);
4697+
PyErr_WriteUnraisable(sni_cb);
46804698
*al = SSL_AD_HANDSHAKE_FAILURE;
46814699
ret = SSL_TLSEXT_ERR_ALERT_FATAL;
46824700
}
@@ -4697,11 +4715,15 @@ _servername_callback(SSL *s, int *al, void *args)
46974715
Py_DECREF(result);
46984716
}
46994717

4718+
Py_DECREF(sni_cb);
4719+
Py_DECREF(sslctx);
47004720
PyGILState_Release(gstate);
47014721
return ret;
47024722

47034723
error:
47044724
Py_XDECREF(ssl_socket);
4725+
Py_DECREF(sni_cb);
4726+
Py_DECREF(sslctx);
47054727
*al = SSL_AD_INTERNAL_ERROR;
47064728
ret = SSL_TLSEXT_ERR_ALERT_FATAL;
47074729
PyGILState_Release(gstate);
@@ -4761,7 +4783,6 @@ _ssl__SSLContext_sni_callback_set_impl(PySSLContext *self, PyObject *value)
47614783
}
47624784
self->set_sni_cb = Py_NewRef(value);
47634785
SSL_CTX_set_tlsext_servername_callback(self->ctx, _servername_callback);
4764-
SSL_CTX_set_tlsext_servername_arg(self->ctx, self);
47654786
}
47664787
return 0;
47674788
}

0 commit comments

Comments
 (0)