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

Activity

  1. picnixz commented on Mar 17, 2026

    @picnixz
    Member

    Is there a less intricate MRE so that we can have a simpler regression test?

  2. devdanzin commented on Mar 17, 2026

    @devdanzin
    MemberAuthor

    I'll try to create one.

  3. devdanzin commented on Mar 17, 2026

    @devdanzin
    MemberAuthor

    This is a bit better suited for a regression test, albeit still long:

    import gc
    import ssl
    import 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')
    
    # Keep a reference to the internal C-level _SSLSocket
    server_internal = server_sslobj._sslobj
    
    # Client sends ClientHello
    try:
        client_sslobj.do_handshake()
    except ssl.SSLWantReadError:
        pass
    server_in.write(client_out.read())
    
    # Kill the SSLObject wrapper — the owner weakref in the C object dies
    del server_sslobj
    gc.collect()
    
    # Trigger the SNI callback with a dead owner weakref → Py_DECREF(NULL)
    server_internal.do_handshake()  # crashes here

    Is it good enough, or should I keep trying to get it smaller?

  4. picnixz commented on Mar 17, 2026

    @picnixz
    Member

    This one seems fine.

  5. picnixz commented on Mar 18, 2026

    @picnixz
    Member

    Do you want to make a PR or should I address it myself? I will have some time today so I can do it.

    @devdanzin

  6. devdanzin commented on Mar 18, 2026

    @devdanzin
    MemberAuthor

    Please handle it, I'll be busy working on more reports.

  7. vstinner commented on Mar 18, 2026

    @vstinner
    Member

    I confirm that the following change does fix the crash:

    diff --git a/Modules/_ssl.c b/Modules/_ssl.c
    index 2eb31229a9b..61282484185 100644
    --- a/Modules/_ssl.c
    +++ b/Modules/_ssl.c
    @@ -5194,7 +5194,7 @@ _servername_callback(SSL *s, int *al, void *args)
         return ret;
     
     error:
    -    Py_DECREF(ssl_socket);
    +    Py_XDECREF(ssl_socket);
         *al = SSL_AD_INTERNAL_ERROR;
         ret = SSL_TLSEXT_ERR_ALERT_FATAL;
         PyGILState_Release(gstate);

    The non-trivial part is to write a regression test :-)

    @picnixz plans to write a PR to fix this issue.

  8. added
    3.13only security fixes
    3.14bugs and security fixes
    3.15bugs and security fixes
    on Mar 18, 2026
  9. added a commit that references this issue on Mar 28, 2026
  10. removed their assignment
    on Mar 28, 2026
  11. picnixz commented on Mar 28, 2026

    @picnixz
  12. picnixz commented on Mar 28, 2026

    @picnixz
  13. added 5 commits that reference this issue on Mar 29, 2026
  14. picnixz commented on Mar 29, 2026

    @picnixz
    Member

    Thanks for the report!

  15. added a commit that references this issue on Apr 16, 2026
  16. added a commit that references this issue on Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.13only security fixes3.14bugs and security fixes3.15bugs 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