Skip to content

Instantly share code, notes, and snippets.

@devdanzin
Created March 18, 2026 10:07
Show Gist options
  • Select an option

  • Save devdanzin/bc8816ef316b72e2baf401f42b209823 to your computer and use it in GitHub Desktop.

Select an option

Save devdanzin/bc8816ef316b72e2baf401f42b209823 to your computer and use it in GitHub Desktop.
_ssl.c: Py_DECREF(NULL) crash in SNI callback when SSLObject wrapper is deleted

_ssl.c: Py_DECREF(NULL) crash in SNI callback when SSLObject wrapper is deleted

CPython issue: python/cpython#146080

Summary

_servername_callback in Modules/_ssl.c (line 5197) calls Py_DECREF(ssl_socket) on the error label, but ssl_socket can be NULL when the SSLObject wrapper has been garbage collected while the C-level _SSLSocket still exists. The owner weakref dies, PyWeakref_GetRef returns 0, ssl_socket is set to NULL at line 5127, and the goto error at line 5128 jumps to Py_DECREF(NULL) — a guaranteed segfault.

Fix

Change Py_DECREF(ssl_socket) to Py_XDECREF(ssl_socket) at line 5197.

Affected versions

CPython main branch (3.15.0a6). Likely affects 3.13+ (the code structure has been stable).

Reproducer

import gc, ssl, test.support

CERTFILE = test.support.findfile("keycert.pem", subdir="certdata")

server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_ctx.load_cert_chain(CERTFILE)
server_ctx.set_servername_callback(lambda *a: None)

client_ctx = ssl.create_default_context()
client_ctx.check_hostname = False
client_ctx.verify_mode = ssl.CERT_NONE

server_in, server_out = ssl.MemoryBIO(), ssl.MemoryBIO()
client_in, client_out = ssl.MemoryBIO(), ssl.MemoryBIO()

server_sslobj = server_ctx.wrap_bio(server_in, server_out, server_side=True)
client_sslobj = client_ctx.wrap_bio(client_in, client_out,
                                     server_side=False, server_hostname='test')

server_internal = server_sslobj._sslobj

try:
    client_sslobj.do_handshake()
except ssl.SSLWantReadError:
    pass
server_in.write(client_out.read())

del server_sslobj
gc.collect()

server_internal.do_handshake()  # crashes: Py_DECREF(NULL)

ASan output

==PID==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000
    #0 Py_DECREF Include/refcount.h:390
    #1 _servername_callback Modules/_ssl.c:5197

Analysis

The error label at line 5196 unconditionally calls Py_DECREF(ssl_socket) but ssl_socket is NULL when the weakref target has been collected. The normal (non-error) path at line 5162 correctly calls Py_DECREF(ssl_socket) only after confirming it's non-NULL (line 5130 onwards).

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