-
-
Notifications
You must be signed in to change notification settings - Fork 33.2k
Description
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
Projects
Status