Skip to content

Regression in ssl module between 3.13.5 and 3.13.6: reading from a TLS-encrypted connection blocks #137583

@aaugustin

Description

@aaugustin

Bug report

Bug description:

The script below works with 3.13.5 and fails with 3.13.6.

It's a straightforward socket server and client with TLS enabled. Under 3.13.5, it runs successfully. Under 3.13.6, when the server calls recv(), it blocks and never receives what the client sent with sendall().

This is a minimal reproduction version of python-websockets/websockets#1648. I performed the reproduction on macOS while the person reporting the bug was on Linux so I think it's platform-independent.

To trigger the bug, the client must read from the connection in a separate thread. If you remove that thread, the bug doesn't happen. (For context, I do this because websockets is architecture with a Sans-I/O layer so I need a background thread to pump bytes received from the network into the Sans-I/O parser.)

Before you run the script, you must download https://github.com/python-websockets/websockets/blob/main/tests/test_localhost.pem and store it next to the file where you saved the Python script.

import os
import socket
import ssl
import threading


TLS_HANDSHAKE_TIMEOUT = 1

print("If Python locks hard:")
print("kill -TERM", os.getpid())
print()

# Create TLS contexts with a self-signed certificate. Download it here:
# https://github.com/python-websockets/websockets/blob/main/tests/test_localhost.pem

server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
server_context.load_cert_chain(b"test_localhost.pem")
# Work around https://github.com/openssl/openssl/issues/7967
server_context.num_tickets = 0

client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client_context.load_verify_locations(b"test_localhost.pem")


# Start a socket server. Nothing fancy here. In a realistic server, we would
# have `serve_forever` with a `while True:` loop. For a minimal reproduction,
# `serve_one` is enough, as the bug occurs on the first request.

server_sock = socket.create_server(("localhost", 0))
server_port = server_sock.getsockname()[1]

server_sock = server_context.wrap_socket(
    server_sock,
    server_side=True,
    # Delay TLS handshake until after we set a timeout on the socket.
    do_handshake_on_connect=False,
)

def conn_handler(sock, addr) -> None:
    print("server accepted connection from", addr)
    sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)

    sock.settimeout(TLS_HANDSHAKE_TIMEOUT)
    assert isinstance(sock, ssl.SSLSocket)
    sock.do_handshake()
    sock.settimeout(None)

    handshake = sock.recv(4096)
    print("server rcvd:")
    print(handshake.decode())
    print()

def serve_one():
    sock, addr = server_sock.accept()
    handler_thread = threading.Thread(target=conn_handler, args=(sock, addr))
    handler_thread.start()

print("server listening on port", server_port)
server_thread = threading.Thread(target=serve_one)
server_thread.start()


# Connect a client to the server. Again, nothing fancy.

client_sock = socket.create_connection(("localhost", server_port))
client_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)

client_sock.settimeout(TLS_HANDSHAKE_TIMEOUT)
client_sock = client_context.wrap_socket(
    client_sock,
    server_hostname="localhost",
)
client_sock.settimeout(None)

### The bug happens only when we're reading from the client socket too! ###

def recv_one_event():
    msg = client_sock.recv(4096)
    print("client rcvd:")
    print(msg.decode())
    print()

client_background_thread = threading.Thread(target=recv_one_event)
client_background_thread.start()

### If you remove client_background_thread.start(), it doesn't happen. ###

handshake = (
    b"GET / HTTP/1.1\r\n"
    b"Host: 127.0.0.1:51970\r\n"
    b"Upgrade: websocket\r\n"
    b"Connection: Upgrade\r\n"
    b"Sec-WebSocket-Key: jjSVQ7XPjx2GIXKfQ49QDQ==\r\n"
    b"Sec-WebSocket-Version: 13\r\n"
    b"Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n"
    b"User-Agent: Python/3.13 websockets/15.0.1\r\n"
    b"\r\n"
)

print("client send:")
print(handshake.decode())
print()
client_sock.sendall(handshake)

CPython versions tested on:

3.13

Operating systems tested on:

macOS

Linked PRs

Metadata

Metadata

Labels

3.13bugs and security fixes3.15new features, bugs and security fixesextension-modulesC modules in the Modules dirrelease-blockertopic-SSLtype-bugAn unexpected behavior, bug, or error

Projects

Status

In Progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions