Segfault from `Py_DECREF(NULL)` in `_servername_callback` error label in `_ssl.c`
Crash report
What happened?
It's possible to cause a segfault in _servername_callback error label by making ssl_socket to be NULL.
Automated diagnosis:
Bug: Py_DECREF(NULL) crash in _ssl _servername_callback. When the SSL socket/owner weakref has been garbage collected during an SNI callback, ssl_socket is NULL. The error label at line 5197 calls Py_DECREF(ssl_socket) on NULL -> segfault.
Fix: Change Py_DECREF to Py_XDECREF
File: Modules/_ssl.c, line 5197
WARNING: THIS MRE WILL RUN openssl IN A SUBPROCESS AND LEAK THE CERT AND KEY FILES
MRE:
""" WARNING: THIS MRE WILL RUN openssl IN A SUBPROCESS AND LEAK THE CERT AND KEY FILES Strategy: Use SSLObject (not SSLSocket) so the Python-level wrapper can be GC'd independently of the C-level SSL object. In the SNI callback, delete the only reference to the SSLObject and force GC. The weakref in ssl->owner dies, PyWeakref_GetRef returns 0, ssl_socket = NULL, goto error -> Py_DECREF(NULL) -> crash. """ import ssl import gc import tempfile import subprocess def generate_self_signed_cert(): """ Generate a self-signed cert+key for testing. WARNING: THIS RUNS openssl IN A SUBPROCESS AND LEAKS THE CERT AND KEY FILES """ certpath = tempfile.mktemp(suffix='.pem') keypath = tempfile.mktemp(suffix='.key') subprocess.run([ 'openssl', 'req', '-x509', '-newkey', 'rsa:2048', '-keyout', keypath, '-out', certpath, '-days', '1', '-nodes', '-subj', '/CN=test' ], capture_output=True, check=True) return certpath, keypath def sni_callback(sslobj, servername, sslctx): pass certpath, keypath = generate_self_signed_cert() server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) server_ctx.load_cert_chain(certpath, keypath) server_ctx.set_servername_callback(sni_callback) # Approach: Use MemoryBIO-based SSLObject (not socket-based SSLSocket) # to have more control over the object lifecycle. server_incoming = ssl.MemoryBIO() server_outgoing = ssl.MemoryBIO() server_sslobj = server_ctx.wrap_bio( server_incoming, server_outgoing, server_side=True ) client_ctx = ssl.create_default_context() client_ctx.check_hostname = False client_ctx.verify_mode = ssl.CERT_NONE client_incoming = ssl.MemoryBIO() client_outgoing = ssl.MemoryBIO() client_sslobj = client_ctx.wrap_bio( client_incoming, client_outgoing, server_side=False, server_hostname='test' ) # Get the internal _SSLSocket objects server_ssl_internal = server_sslobj._sslobj client_ssl_internal = client_sslobj._sslobj # The _SSLSocket's "owner" weakref points to the SSLObject. # If we delete the SSLObject, the weakref dies. # Try to do the handshake for i in range(20): # Client step try: client_sslobj.do_handshake() except ssl.SSLWantReadError: pass # Transfer client -> server data = client_outgoing.read() if data: server_incoming.write(data) # NOW: before server processes the ClientHello (which triggers SNI), # try to kill the server SSLObject so the weakref dies. if i == 0 and data: # The server hasn't processed ClientHello yet. # Delete the SSLObject wrapper — the internal _SSLSocket # still exists (we hold server_ssl_internal). # The weakref ssl->owner should now be dead. del server_sslobj gc.collect() print(f"server_ssl_internal still alive: {server_ssl_internal is not None}") # Now do_handshake on the internal object directly # This will trigger the SNI callback with a dead owner weakref server_ssl_internal.do_handshake()
Backtrace:
Program received signal SIGSEGV, Segmentation fault.
0x00007bfff59bf197 in Py_DECREF (lineno=5197, op=0x0, filename=<optimized out>) at ./Include/refcount.h:390
390 if (op->ob_refcnt_full <= 0 || op->ob_refcnt > (((PY_UINT32_T)-1) - (1<<20))) {
#0 0x00007bfff59bf197 in Py_DECREF (lineno=5197, op=0x0, filename=<optimized out>) at ./Include/refcount.h:390
#1 _servername_callback (s=0x7e1ff6ff8100, al=<optimized out>, args=0x7d4ff7011930) at ./Modules/_ssl.c:5197
#2 0x00007bfff459d89a in ?? () from /lib/x86_64-linux-gnu/libssl.so.3
#3 0x00007bfff459eddc in ?? () from /lib/x86_64-linux-gnu/libssl.so.3
#4 0x00007bfff45c15f2 in ?? () from /lib/x86_64-linux-gnu/libssl.so.3
#5 0x00007bfff45abdbb in ?? () from /lib/x86_64-linux-gnu/libssl.so.3
#6 0x00007bfff59bff61 in _ssl__SSLSocket_do_handshake_impl (self=0x7caff70e4070) at ./Modules/_ssl.c:1052
#7 _ssl__SSLSocket_do_handshake (self=0x7caff70e4070, _unused_ignored=<optimized out>) at ./Modules/clinic/_ssl.c.h:30
#8 0x0000555555ae6992 in method_vectorcall_NOARGS (func=func@entry=0x7c7ff70f14c0, args=args@entry=0x7bfff5d8c728, nargsf=nargsf@entry=9223372036854775809, kwnames=kwnames@entry=0x0)
at Objects/descrobject.c:448
#9 0x0000555555ab9e00 in _PyObject_VectorcallTstate (tstate=0x5555568f7b18 <_PyRuntime+360664>, callable=0x7c7ff70f14c0, args=0x7bfff5d8c728, nargsf=9223372036854775809, kwnames=0x0)
at ./Include/internal/pycore_call.h:136
#10 0x0000555555e588dd in _Py_VectorCallInstrumentation_StackRefSteal (callable=..., arguments=<optimized out>, total_args=1, kwnames=..., call_instrumentation=<optimized out>,
frame=<optimized out>, this_instr=<optimized out>, tstate=<optimized out>) at Python/ceval.c:770
#11 0x0000555555e94263 in _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=<optimized out>, throwflag=<optimized out>) at Python/generated_cases.c.h:1838
#12 0x0000555555e57778 in _PyEval_EvalFrame (tstate=0x5555568f7b18 <_PyRuntime+360664>, frame=0x7e8ff6fe5220, throwflag=0) at ./Include/internal/pycore_ceval.h:118
#13 _PyEval_Vector (tstate=<optimized out>, func=<optimized out>, locals=<optimized out>, args=<optimized out>, argcount=<optimized out>, kwnames=0x0) at Python/ceval.c:2134
#14 0x0000555555e57195 in PyEval_EvalCode (co=<optimized out>, globals=<optimized out>, locals=0x7c7ff70884c0) at Python/ceval.c:681
#15 0x0000555556061fb0 in run_eval_code_obj (tstate=tstate@entry=0x5555568f7b18 <_PyRuntime+360664>, co=co@entry=0x7d8ff7016690, globals=globals@entry=0x7c7ff70884c0,
locals=locals@entry=0x7c7ff70884c0) at Python/pythonrun.c:1368
#16 0x000055555606117c in run_mod (mod=<optimized out>, filename=<optimized out>, globals=<optimized out>, locals=<optimized out>, flags=<optimized out>, arena=<optimized out>,
interactive_src=<optimized out>, generate_new_source=<optimized out>) at Python/pythonrun.c:1471
Found using cpython-review-toolkit with Claude Opus 4.6, using the /cpython-review-toolkit:explore Modules/_ssl.c all deep command.
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.15.0a7+ (heads/main:99e2c5eccd2, Mar 17 2026, 08:26:50) [Clang 21.1.2 (2ubuntu6)]