Repository navigation
[2.7.18.16] ssl: do not call the SNI callback through a released SSLContext (CVE-2026-19445) - #106
Conversation
…ontext (CVE-2026-19445) Backport of python/cpython pythongh-156293 (87665c9; 3.10: ec44b5a) to 2.7.18.x. set_servername_callback() registered the SSLContext as a borrowed pointer in the SSL_CTX's servername arg, and _servername_callback() used that pointer to find the Python callback. When the callback switches the connection to another context (sslsock.context = other), the original SSLContext can be deallocated while its SSL_CTX lives on as the connection's session_ctx. The callback then reads freed memory: right after the Python callback returns (if it dropped the last reference and raised), or on a later ClientHello such as one after a HelloRetryRequest. On 2.7.18.15, a callback that switches context, drops it and raises segfaults the server. Under captured stderr it reports "in <NULL> ignored", read from the freed object. _servername_callback() now finds the context through the SSL object's app data (the PySSLSocket and its current, live context), and holds strong references to that context and its callback while they are used. set_servername_callback() no longer registers the servername arg, and context_dealloc() unregisters the servername callback before freeing the SSL_CTX. As upstream, after the callback switches contexts, any further ClientHello on that connection goes to the new context's callback. 2.7.18.x: the legacy set_servername_callback()/set_hostname is the only API (there is no sni_callback property); no Py_NewRef; context_dealloc's unregister is guarded by HAVE_SNI like the rest of the SNI code. 2.7 has no MemoryBIO, so the tests run the handshake over a socketpair with the client in a thread. The "callback raises" test also checks that the unraisable error names the callback, which is what fails on 2.7.18.15 ("<NULL>"). The HelloRetryRequest test passes on 2.7.18.15 in this release build; it is kept, as upstream, as a regression guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ezequielp-activestate
left a comment
There was a problem hiding this comment.
No blockers. Approving.
The fix matches upstream's (34a53dc, pythongh-156293 / pythonGH-158504): the callback finds the context through the SSL object's app data and holds strong references to it and to the callback, the borrowed servername arg is no longer registered, and context_dealloc() clears the servername callback before SSL_CTX_free(). I walked upstream's _ssl.c forward from that commit through main and the 3.10–3.14 backports: nothing later touches the servername callback, so there's no follow-up to port.
Reference counting on 2.7's _servername_callback: ssl_ctx and sni_cb are taken after PyGILState_Ensure() and released on all three exits (no callback, success, error:). The callback can replace sslsock.context, which drops the socket's reference to the old context; the local reference keeps it alive until the end of the function, which is what fixes the use-after-free.
Checked under AddressSanitizer (debug build, --with-pydebug --without-pymalloc -fsanitize=address, Linux x86_64, OpenSSL 1.1.1d), running this PR's two new tests against both trees:
| test | 2.7.18.16 |
this PR |
|---|---|---|
test_sni_callback_context_released_and_callback_raises |
heap-use-after-free in _servername_callback (_ssl.c:3490) |
OK |
test_sni_callback_context_released_before_second_client_hello |
heap-use-after-free in _servername_callback (_ssl.c:3445) |
OK |
So the HelloRetryRequest test, which the description says passes on 2.7.18.15 in a release build, does catch the bug: it reads freed memory there too, and ASAN shows it. Full test_ssl: 103 run, 14 skipped, OK (base: 101 run, 14 skipped, OK).
⚪ Pre-existing, not from this PR: when ssl->ssl_sock is NULL, ssl_socket is set to Py_None without a reference and the error: path then does Py_DECREF(ssl_socket), dropping a reference to None it never took. Upstream 3.x avoids this with Py_XDECREF on a NULL ssl_socket. It's harmless in practice, because ssl.SSLSocket always passes itself, but it's a one-line fix if you're in here anyway.
FYI, outside this PR: a stock ./configure && make of the 2.7.18.16 branch builds no _socket (so no ssl). socketmodule.c calls _PyTime_FromSecondsObject, but Python/pytime.c isn't in Makefile.pre.in, and it doesn't compile on Linux as is: it uses _Py_InIntegralTypeRange, Py_BUILD_ASSERT, Py_UNREACHABLE and Py_ABS, and its clock_gettime path needs HAVE_CLOCK_GETTIME, which 2.7's configure never defines. I worked around it locally to run these tests. This doesn't matter if release builds patch it, but anyone building the branch from source will hit it.
Not assessed: Windows, and OpenSSL 3.x.
Backport of upstream pythongh-156293 (CVE-2026-19445): a use-after-free in the ssl SNI (servername) callback. Part of 2.7.18.16 (Linear PROD-1588).
Problem
set_servername_callback()stores theSSLContextas a borrowed pointer in the SSL_CTX's servername arg, and_servername_callback()reads the Python callback through that pointer. A callback can switch the connection to another context (sslsock.context = other). If the originalSSLContextis then released, its SSL_CTX lives on as the connection's session_ctx, and the callback reads freed memory. That happens right after the Python callback returns (when it dropped the last reference and raised), or on a later ClientHello (after a HelloRetryRequest).On 2.7.18.15, a server whose callback switches context, drops it and raises segfaults:
Only servers that let go of their dispatching context are affected.
Fix
_servername_callback()finds the context through the SSL object's app data (the socket's current, always-live context), and holds strong references to it and its callback while they're in use.context_dealloc()unregisters the servername callback before freeing the SSL_CTX.Behavior change (same as upstream): after the callback switches contexts, any later ClientHello on that connection goes to the new context's callback.
2.7 adaptation: 2.7 only has the legacy
set_servername_callback()API (set_hostname) and noMemoryBIO. The tests therefore run the handshake over asocketpair()with the client in a thread.Testing (Linux x86_64, prod OpenSSL 1.1.1w)
test_sni_callback_context_released_and_callback_raises(new): the callback switches context, drops it and raises. The test asserts that the handshake fails with SSLError, that the unraisable error names the callback, that the dispatch context was freed, and thatserver.contextis the new context. On 2.7.18.15 it fails, because the message says<NULL>read from the freed context; outside the test's captured stderr, the same scenario segfaults. It passes with this change.test_sni_callback_context_released_before_second_client_hello(new): forces a HelloRetryRequest with a P-384-only server. It passes with this change. It also passes on 2.7.18.15 in this release build, so it's kept, as upstream, as a regression guard.test_ssl(-uall): 117 tests run, onlyNetworkedTests.test_context_setgetfails. It needs the network and fails the same way on 2.7.18.15.