Skip to content

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

@devdanzin

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 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)]

Metadata

Metadata

Assignees

No one assigned

    Labels

    extension-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