Skip to content

Segfault from Py_DECREF(NULL) in _servername_callback error label in _ssl.c #146080

Description

@devdanzin

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

Full report

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 FILESStrategy: Use SSLObject (not SSLSocket) so the Python-level wrapper canbe GC'd independently of the C-level SSL object. In the SNI callback,delete the only reference to the SSLObject and force GC. The weakrefin ssl->owner dies, PyWeakref_GetRef returns 0, ssl_socket = NULL,goto error -> Py_DECREF(NULL) -> crash."""importsslimportgcimporttempfileimportsubprocessdefgenerate_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)
returncertpath, keypathdefsni_callback(sslobj, servername, sslctx):
passcertpath, 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=Falseclient_ctx.verify_mode=ssl.CERT_NONEclient_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 objectsserver_ssl_internal=server_sslobj._sslobjclient_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 handshakeforiinrange(20):
# Client steptry:
client_sslobj.do_handshake()
exceptssl.SSLWantReadError:
pass# Transfer client -> serverdata=client_outgoing.read()
ifdata:
server_incoming.write(data)
# NOW: before server processes the ClientHello (which triggers SNI),# try to kill the server SSLObject so the weakref dies.ifi==0anddata:
# 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.delserver_sslobjgc.collect()
print(f"server_ssl_internal still alive: {server_ssl_internalisnotNone}")
# Now do_handshake on the internal object directly# This will trigger the SNI callback with a dead owner weakrefserver_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)]

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.13bugs and security fixes3.14bugs and security fixes3.15pre-release feature fixes, bugs and security fixesextension-modulesC modules in the Modules dirtopic-SSLtype-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions