Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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

Copy link
Copy link
@devdanzin

Description

@devdanzin
Issue body actions

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

Linked PRs

Reactions are currently unavailable

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.13bugs and security fixesbugs and security fixes3.14bugs and security fixesbugs and security fixes3.15pre-release feature fixes, bugs and security fixespre-release feature fixes, bugs and security fixesextension-modulesC modules in the Modules dirC modules in the Modules dirtopic-SSLtype-crashA hard crash of the interpreter, possibly with a core dumpA hard crash of the interpreter, possibly with a core dump
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Morty Proxy This is a proxified and sanitized view of the page, visit original site.