From 78574efea9a2b5ac24f86710eedc5636d2502fdd Mon Sep 17 00:00:00 2001 From: Mahiro Sakaue Date: Sat, 17 Oct 2026 02:58:36 +0900 Subject: [PATCH] gh-148594: Clear stale OpenSSL errors before TLS. I/O. --- Lib/test/test_ssl.py | 84 +++++++++++++++++++ ...-10-16-18-00-00.gh-issue-148594.kG7pQx.rst | 2 + Modules/_ssl.c | 5 ++ 3 files changed, 91 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-16-18-00-00.gh-issue-148594.kG7pQx.rst diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index d8eebc33deebfab..392f6114c41ee1b 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -1029,6 +1029,51 @@ def test_read_write_zero(self): self.assertEqual(s.recv(0), b"") self.assertEqual(s.send(b""), 0) + def test_write_with_stale_error_queue(self): + client_socket, server_socket = socket.socketpair() + self.addCleanup(client_socket.close) + self.addCleanup(server_socket.close) + client_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 16384) + client_socket.setblocking(False) + server_socket.setblocking(False) + client_context, server_context, hostname = testing_context() + client = self.enterContext(client_context.wrap_socket( + client_socket, server_hostname=hostname, + do_handshake_on_connect=False)) + server = self.enterContext(server_context.wrap_socket( + server_socket, server_side=True, do_handshake_on_connect=False)) + + pending = [client, server] + for _ in support.busy_retry(support.LOOPBACK_TIMEOUT): + for conn in pending[:]: + try: + conn.do_handshake() + except (ssl.SSLWantReadError, ssl.SSLWantWriteError): + pass + else: + pending.remove(conn) + if not pending: + break + + # Settle the post-handshake state before testing the write. + with self.assertRaises(ssl.SSLWantReadError): + client.read(1) + + # The peer does not read, so fill the socket's send buffer. + data = b'x' * 16384 + for _ in range(256): + try: + client.write(data) + except ssl.SSLWantWriteError: + break + else: + self.fail("could not fill the socket's send buffer") + + with self.assertRaises(ValueError): + _ssl.nid2obj(9999999) + with self.assertRaises(ssl.SSLWantWriteError): + client.write(data) + class ContextTests(unittest.TestCase): @@ -2270,6 +2315,45 @@ def test_unwrap(self): c_in.write(s_out.read()) client.unwrap() + def test_read_with_stale_error_queue(self): + # gh-148594: SSL_get_error() checks the thread's OpenSSL error queue + # before the state of the SSL object. An error left in the queue by + # an earlier, unrelated call made a read that would block raise + # SSLError or OSError instead of SSLWantReadError. + client_context, server_context, hostname = testing_context() + client, *_ = connected_bio_pair(client_context, server_context, + hostname) + # The first read processes the post-handshake messages, which clears + # the queue by itself, so read once before leaving errors in it. + with self.assertRaises(ssl.SSLWantReadError): + client.read(1) + + # Each of these calls fails and leaves its error in the queue. + failing_calls = [ + (ValueError, _ssl.nid2obj, 9999999), + (ValueError, _ssl.txt2obj, 'bogus'), + (ssl.SSLError, _ssl._test_decode_cert, NONEXISTINGCERT), + ] + for exc_type, func, arg in failing_calls: + with self.subTest(func=func.__name__): + with self.assertRaises(exc_type): + func(arg) + with self.assertRaises(ssl.SSLWantReadError): + client.read(1) + + def test_unwrap_with_stale_error_queue(self): + client_context, server_context, hostname = testing_context() + client, *_ = connected_bio_pair(client_context, server_context, + hostname) + with self.assertRaises(ssl.SSLWantReadError): + client.read(1) + + with self.assertRaises(ValueError): + _ssl.nid2obj(9999999) + # The peer has not sent close_notify, so shutdown must wait for it. + with self.assertRaises(ssl.SSLWantReadError): + client.unwrap() + def test_sni_callback_context_released_and_callback_raises(self): # Variant of the test below without a HelloRetryRequest: the callback # switches the connection to another context, drops the last diff --git a/Misc/NEWS.d/next/Library/2026-10-16-18-00-00.gh-issue-148594.kG7pQx.rst b/Misc/NEWS.d/next/Library/2026-10-16-18-00-00.gh-issue-148594.kG7pQx.rst new file mode 100644 index 000000000000000..e1377e1af502c44 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-16-18-00-00.gh-issue-148594.kG7pQx.rst @@ -0,0 +1,2 @@ +Prevent stale OpenSSL errors from causing spurious exceptions during TLS I/O +in the :mod:`ssl` module by clearing the error queue before each operation. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 5fc029af24821a9..ab41396903d16c1 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -1069,6 +1069,7 @@ _ssl__SSLSocket_do_handshake_impl(PySSLSocket *self) /* XXX If SSL_do_handshake() returns 0, it's also a failure. */ do { Py_BEGIN_ALLOW_THREADS + ERR_clear_error(); ret = SSL_do_handshake(self->ssl); err = _PySSL_errno(ret < 1, self->ssl, ret); Py_END_ALLOW_THREADS; @@ -2671,6 +2672,7 @@ _ssl__SSLSocket_sendfile_impl(PySSLSocket *self, int fd, Py_off_t offset, do { Py_BEGIN_ALLOW_THREADS + ERR_clear_error(); retval = SSL_sendfile(self->ssl, fd, (off_t)offset, size, flags); err = _PySSL_errno(retval < 0, self->ssl, (int)retval); Py_END_ALLOW_THREADS; @@ -2802,6 +2804,7 @@ _ssl__SSLSocket_write_impl(PySSLSocket *self, Py_buffer *b) do { Py_BEGIN_ALLOW_THREADS; + ERR_clear_error(); retval = SSL_write_ex(self->ssl, b->buf, (size_t)b->len, &count); err = _PySSL_errno(retval == 0, self->ssl, retval); Py_END_ALLOW_THREADS; @@ -2959,6 +2962,7 @@ _ssl__SSLSocket_read_impl(PySSLSocket *self, Py_ssize_t len, do { Py_BEGIN_ALLOW_THREADS; + ERR_clear_error(); retval = SSL_read_ex(self->ssl, mem, (size_t)len, &count); err = _PySSL_errno(retval == 0, self->ssl, retval); Py_END_ALLOW_THREADS; @@ -3070,6 +3074,7 @@ _ssl__SSLSocket_shutdown_impl(PySSLSocket *self) */ if (self->shutdown_seen_zero) SSL_set_read_ahead(self->ssl, 0); + ERR_clear_error(); ret = SSL_shutdown(self->ssl); err = _PySSL_errno(ret < 0, self->ssl, ret); Py_END_ALLOW_THREADS;