Skip to content

[2.7.18.16] ssl: do not call the SNI callback through a released SSLContext (CVE-2026-19445) - #106

Merged
icanhasmath merged 1 commit into
2.7.18.16from
2.7.18.16-ssl-sni-uaf
Oct 28, 2026
Merged

icanhasmath merged 1 commit into
2.7.18.16from
2.7.18.16-ssl-sni-uaf

Conversation

@icanhasmath

Copy link
Copy Markdown

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 the SSLContext as 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 original SSLContext is 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:

Exception LookupError: LookupError("no certificate for u'localhost'",) in <NULL> ignored
Segmentation fault (core dumped)

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.
  • The borrowed servername arg is no longer registered.
  • 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 no MemoryBIO. The tests therefore run the handshake over a socketpair() 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 that server.context is 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.
  • Full test_ssl (-uall): 117 tests run, only NetworkedTests.test_context_setget fails. It needs the network and fails the same way on 2.7.18.15.
  • Windows x64: not built here.

…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 ezequielp-activestate left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants