Repository navigation
On OpenSSL 4, SSL_read_ex() fails with a different error on EOF #30894
Description
Hi,
I'm updating Python to OpenSSL 4 and I get a different error between OpenSSL 3 (3.5.4) and OpenSSL 4 (4.0.0) on SSL_read_ex() on the TLS client after the TLS server closes the socket.
When the server closes the socket (with sslconn.shutdown(socket.SHUT_WR); sslconn.close()), the first SSL_read_ex() on the client fails with SSL_R_UNEXPECTED_EOF_WHILE_READING (same behavior on OpenSSL 3 and OpenSSL 4):
SSL_get_error()=SSL_ERROR_SSLERR_GET_LIB()=ERR_LIB_SSLERR_GET_REASON()=SSL_R_UNEXPECTED_EOF_WHILE_READING
This error is helpful. My problem is on the second SSL_read_ex() on the client, I get a different error between OpenSSL 3 and OpenSSL 4:
- OpenSSL 3:
SSL_get_error()=SSL_ERROR_SYSCALL,ERR_peek_last_error()=0. Python raises aSSLEOFErrorin this case (behaves correctly). - OpenSSL 4:
SSL_get_error()=SSL_ERROR_SSL,ERR_peek_last_error()=0. Python raises a genericSSLError("A failure in the SSL library occurred")in this case (which is unexpected).
OpenSSL 4 sets a generic SSL_ERROR_SSL error with ERR_peek_last_error()=0. It's not helpful to detect an "EOF".
My question is if the different error on OpenSSL 4 was made on purpose, or if it's an issue in OpenSSL 4?
If you want to reproduce my issue, get the Python script:
Details
import os.path
import socket
import ssl
import threading
CERT = os.path.join('Lib', 'test', 'certdata', 'keycert.pem')
HOST = '127.0.0.1'
HOSTNAME = 'localhost'
class Server(threading.Thread):
def __init__(self):
super().__init__()
self.listening = threading.Event()
self.address = None
def run(self):
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(CERT)
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
#server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
server_sock.bind((HOST, 0))
server_sock.listen(5)
self.address = server_sock.getsockname()
self.listening.set()
sock, addr = server_sock.accept()
sslconn = context.wrap_socket(sock, server_side=True)
request = b''
while True:
chunk = sslconn.recv(65537)
request += chunk
if b'\r\n\r\n' in request:
break
print(f"server got request: {request!r}")
print("server sendall")
sslconn.sendall(
b'HTTP/1.0 200 OK\r\n'
b'Server: TestHTTP/ Python/3.15.0a8+\r\n'
b'Date: Thu, 16 Apr 2026 12:42:37 GMT\r\n'
b'Content-type: text/plain\r\n\r\n')
sslconn.sendall(b'we care a bit')
print("server shutdown write")
sslconn.shutdown(socket.SHUT_WR)
print("server close socket")
sslconn.close()
server_sock.close()
def main():
server = Server()
server.start()
server.listening.wait()
port = server.address[1]
context = ssl.create_default_context(cafile=CERT)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, port))
sslsock = context.wrap_socket(sock, server_hostname=HOSTNAME)
sslsock.sendall(b'GET /bizarre HTTP/1.0\r\n\r\n')
sslobj = sslsock._sslobj
def read(prefix, sslobj):
try:
data = sslobj.read(1024)
result = repr(data)
except ssl.SSLError as exc:
result = f'<{exc!r}>'
print(prefix, result)
for i in range(1, 5):
read(f"client read #{i}:", sslobj)
sslsock.close()
server.join()
if __name__ == "__main__":
main()And run these commands on Linux:
git clone https://github.com/python/cpython --depth=1
cd cpython/
# Build OpenSSL 4 in ~/multissl/openssl/4.0.0/
python3 Tools/ssl/multissltests.py --steps=library --base-directory $HOME/multissl --openssl 4.0.0 --system Linux
./configure --with-pydebug --with-openssl=$HOME/multissl/openssl/4.0.0/
LD_LIBRARY_PATH=~/multissl/openssl/4.0.0/lib64/ make
LD_LIBRARY_PATH=~/multissl/openssl/4.0.0/lib64/ ./python script.py Victor
Activity
mattcaswell commentedon Apr 20, 2026
@igus68
igus68 commentedon Apr 20, 2026
We have discussed it already in #30631:
when a read or write function is called after the state machine failed into the MSG_FLOW_ERROR state, it is handled as a protocol error.
picnixz commentedon Apr 21, 2026
Maybe I missed the decision in the other discussion, but should the error queue be empty in this case, even after a protocol failure? how do you suggest we should handle this in Python (Victor suggested to remember whether we hit an EOF to reraise it)?
igus68 commentedon Apr 22, 2026
Could you please clarify your use case? After the first SSL_read_ex() returned a fatal error, what is the reason to call it again? The connection no longer exists, which is why the second read fails. We might push some error like "the connection is in the fatal fail state", but would it be more helpful?
However, if you really need to call SSL_read_ex() again and want to receive the same error as after the first read, I can suggest not removing the error code from the stack after the first read (use ERR_peek_error() instead of ERR_get_error()).