Skip to content

Commit 99ef892

Browse files
sethmlarsongpshead
andcommitted
[3.12] gh-156293: Use-after-free for server-side SSLContext with sni_callback
Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
1 parent f30b0d1 commit 99ef892

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

1669+
If the callback assigns a new context to :attr:`SSLSocket.context`, any
1670+
further ClientHello message on the same connection (for example after a
1671+
TLS 1.3 HelloRetryRequest) is dispatched to the new context's
1672+
*sni_callback*, if it has one; the original callback is not called again
1673+
for that connection.
1674+
16691675
Due to the early negotiation phase of the TLS connection, only limited
16701676
methods and attributes are usable like
16711677
:meth:`SSLSocket.selected_alpn_protocol` and :attr:`SSLSocket.context`.
@@ -1689,6 +1695,11 @@ to speed up repeated connections from the same clients.
16891695

16901696
.. versionadded:: 3.7
16911697

1698+
.. versionchanged:: next
1699+
After the callback assigns a new :attr:`SSLSocket.context`, later
1700+
ClientHello messages on the connection are dispatched to the new
1701+
context's *sni_callback*.
1702+
16921703
.. attribute:: SSLContext.set_servername_callback(server_name_callback)
16931704

16941705
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
@@ -1818,6 +1818,86 @@ def test_unwrap(self):
18181818
c_in.write(s_out.read())
18191819
client.unwrap()
18201820

1821+
def test_sni_callback_context_released_and_callback_raises(self):
1822+
# Variant of the test below without a HelloRetryRequest: the callback
1823+
# switches the connection to another context, drops the last
1824+
# references to the context that carries it, and raises. The C
1825+
# callback must not touch that context after the Python callback
1826+
# returned.
1827+
client_ctx, server_ctx, hostname = testing_context()
1828+
leaf_ctx = server_ctx
1829+
1830+
def sni_cb(sslobj, server_name, ctx):
1831+
sslobj.context = leaf_ctx
1832+
del ctx
1833+
raise LookupError("no certificate for " + repr(server_name))
1834+
1835+
def make_server():
1836+
dispatch_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
1837+
dispatch_ctx.load_cert_chain(SIGNED_CERTFILE)
1838+
dispatch_ctx.sni_callback = sni_cb
1839+
s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO()
1840+
server = dispatch_ctx.wrap_bio(s_in, s_out, server_side=True)
1841+
return server, s_in, s_out
1842+
1843+
server, s_in, s_out = make_server()
1844+
c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO()
1845+
client = client_ctx.wrap_bio(c_in, c_out, server_hostname=hostname)
1846+
with self.assertRaises(ssl.SSLWantReadError):
1847+
client.do_handshake()
1848+
s_in.write(c_out.read())
1849+
with support.catch_unraisable_exception() as cm:
1850+
with self.assertRaises(ssl.SSLError):
1851+
server.do_handshake()
1852+
self.assertIsInstance(cm.unraisable.exc_value, LookupError)
1853+
self.assertIs(server.context, leaf_ctx)
1854+
1855+
def test_sni_callback_context_released_before_second_client_hello(self):
1856+
# The SSLContext carrying sni_callback may be released by the
1857+
# application once the callback has switched the connection over to
1858+
# another context. If the server then sends a HelloRetryRequest, the
1859+
# second ClientHello makes OpenSSL consult the original SSL_CTX's
1860+
# servername callback again; that must not use the deallocated
1861+
# SSLContext object.
1862+
client_ctx, leaf_ctx, hostname = testing_context()
1863+
calls = []
1864+
1865+
def make_server():
1866+
dispatch_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
1867+
dispatch_ctx.load_cert_chain(SIGNED_CERTFILE)
1868+
# Force a HelloRetryRequest: the client offers an X25519 key
1869+
# share first, the server only accepts P-384.
1870+
dispatch_ctx.set_ecdh_curve("secp384r1")
1871+
def sni_cb(sslobj, server_name, ctx):
1872+
calls.append(server_name)
1873+
sslobj.context = leaf_ctx
1874+
dispatch_ctx.sni_callback = sni_cb
1875+
s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO()
1876+
server = dispatch_ctx.wrap_bio(s_in, s_out, server_side=True)
1877+
return server, s_in, s_out, weakref.ref(dispatch_ctx)
1878+
1879+
# After this only the C-level SSL object references dispatch_ctx.
1880+
server, s_in, s_out, dispatch_ref = make_server()
1881+
c_in, c_out = ssl.MemoryBIO(), ssl.MemoryBIO()
1882+
client = client_ctx.wrap_bio(c_in, c_out, server_hostname=hostname)
1883+
for _ in range(10):
1884+
for obj, out, peer_in in ((client, c_out, s_in),
1885+
(server, s_out, c_in)):
1886+
try:
1887+
obj.do_handshake()
1888+
except ssl.SSLWantReadError:
1889+
pass
1890+
if out.pending:
1891+
peer_in.write(out.read())
1892+
client.do_handshake()
1893+
server.do_handshake()
1894+
support.gc_collect()
1895+
self.assertIsNone(dispatch_ref())
1896+
self.assertGreaterEqual(len(calls), 1)
1897+
self.assertEqual(calls[0], hostname)
1898+
self.assertIs(server.context, leaf_ctx)
1899+
self.assertIsNotNone(client.cipher())
1900+
18211901
class SimpleBackgroundTests(unittest.TestCase):
18221902
"""Tests that connect to a simple server running in the background"""
18231903

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: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3184,6 +3184,9 @@ context_dealloc(PySSLContext *self)
31843184
/* bpo-31095: UnTrack is needed before calling any callbacks */
31853185
PyObject_GC_UnTrack(self);
31863186
context_clear(self);
3187+
/* The SSL_CTX may outlive this object as the session_ctx of sockets that
3188+
were switched to another context; leave no Python callback behind. */
3189+
SSL_CTX_set_tlsext_servername_callback(self->ctx, NULL);
31873190
SSL_CTX_free(self->ctx);
31883191
PyMem_FREE(self->alpn_protocols);
31893192
Py_TYPE(self)->tp_free(self);
@@ -4346,27 +4349,37 @@ _ssl__SSLContext_set_ecdh_curve(PySSLContext *self, PyObject *name)
43464349
}
43474350

43484351
static int
4349-
_servername_callback(SSL *s, int *al, void *args)
4352+
_servername_callback(SSL *s, int *al, void *Py_UNUSED(args))
43504353
{
43514354
int ret;
4352-
PySSLContext *sslctx = (PySSLContext *) args;
4355+
PySSLContext *sslctx;
43534356
PySSLSocket *ssl;
43544357
PyObject *result;
43554358
/* The high-level ssl.SSLSocket object */
43564359
PyObject *ssl_socket;
4360+
PyObject *sni_cb;
43574361
const char *servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name);
43584362
PyGILState_STATE gstate = PyGILState_Ensure();
43594363

4360-
if (sslctx->set_sni_cb == NULL) {
4361-
/* remove race condition in this the call back while if removing the
4362-
* callback is in progress */
4364+
/* Do not use the SSL_CTX's servername arg to find the context: it is a
4365+
borrowed pointer to whichever _SSLContext installed the callback, and
4366+
that object may already be gone while OpenSSL still reaches this
4367+
callback through the connection's session_ctx (e.g. on the second
4368+
ClientHello after a HelloRetryRequest, once sni_callback has switched
4369+
the socket to another context). The socket's current context is
4370+
always alive; hold strong references to it and to the callback while
4371+
they are used here. */
4372+
ssl = SSL_get_app_data(s);
4373+
assert(ssl != NULL);
4374+
sslctx = (PySSLContext *)Py_NewRef(ssl->ctx);
4375+
assert(Py_IS_TYPE(ssl, get_state_ctx(sslctx)->PySSLSocket_Type));
4376+
sni_cb = Py_XNewRef(sslctx->set_sni_cb);
4377+
if (sni_cb == NULL) {
4378+
Py_DECREF(sslctx);
43634379
PyGILState_Release(gstate);
43644380
return SSL_TLSEXT_ERR_OK;
43654381
}
43664382

4367-
ssl = SSL_get_app_data(s);
4368-
assert(Py_IS_TYPE(ssl, get_state_ctx(sslctx)->PySSLSocket_Type));
4369-
43704383
/* The servername callback expects an argument that represents the current
43714384
* SSL connection and that has a .context attribute that can be changed to
43724385
* identify the requested hostname. Since the official API is the Python
@@ -4387,7 +4400,7 @@ _servername_callback(SSL *s, int *al, void *args)
43874400
goto error;
43884401

43894402
if (servername == NULL) {
4390-
result = PyObject_CallFunctionObjArgs(sslctx->set_sni_cb, ssl_socket,
4403+
result = PyObject_CallFunctionObjArgs(sni_cb, ssl_socket,
43914404
Py_None, sslctx, NULL);
43924405
}
43934406
else {
@@ -4410,14 +4423,14 @@ _servername_callback(SSL *s, int *al, void *args)
44104423
}
44114424
Py_DECREF(servername_bytes);
44124425
result = PyObject_CallFunctionObjArgs(
4413-
sslctx->set_sni_cb, ssl_socket, servername_str,
4426+
sni_cb, ssl_socket, servername_str,
44144427
sslctx, NULL);
44154428
Py_DECREF(servername_str);
44164429
}
44174430
Py_DECREF(ssl_socket);
44184431

44194432
if (result == NULL) {
4420-
PyErr_WriteUnraisable(sslctx->set_sni_cb);
4433+
PyErr_WriteUnraisable(sni_cb);
44214434
*al = SSL_AD_HANDSHAKE_FAILURE;
44224435
ret = SSL_TLSEXT_ERR_ALERT_FATAL;
44234436
}
@@ -4438,11 +4451,15 @@ _servername_callback(SSL *s, int *al, void *args)
44384451
Py_DECREF(result);
44394452
}
44404453

4454+
Py_DECREF(sni_cb);
4455+
Py_DECREF(sslctx);
44414456
PyGILState_Release(gstate);
44424457
return ret;
44434458

44444459
error:
44454460
Py_DECREF(ssl_socket);
4461+
Py_DECREF(sni_cb);
4462+
Py_DECREF(sslctx);
44464463
*al = SSL_AD_INTERNAL_ERROR;
44474464
ret = SSL_TLSEXT_ERR_ALERT_FATAL;
44484465
PyGILState_Release(gstate);
@@ -4480,7 +4497,6 @@ set_sni_callback(PySSLContext *self, PyObject *arg, void *c)
44804497
}
44814498
self->set_sni_cb = Py_NewRef(arg);
44824499
SSL_CTX_set_tlsext_servername_callback(self->ctx, _servername_callback);
4483-
SSL_CTX_set_tlsext_servername_arg(self->ctx, self);
44844500
}
44854501
return 0;
44864502
}

0 commit comments

Comments
 (0)