From 9c10ead0d03254cfdfbe2bbdb8d5da688506b4e9 Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Wed, 15 Apr 2026 11:33:57 +0300 Subject: [PATCH 1/4] gh-148594: Clear OpenSSL error queue before SSL read/write --- Lib/test/test_ssl.py | 63 +++++++++++++++++++ ...-04-15-11-33-29.gh-issue-148594.4PdWRt.rst | 3 + Modules/_ssl.c | 2 + 3 files changed, 68 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-04-15-11-33-29.gh-issue-148594.4PdWRt.rst diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 01b26bbf794764..2a8f4de2089356 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -35,6 +35,7 @@ from contextlib import nullcontext try: import ctypes + import ctypes.util except ImportError: ctypes = None @@ -319,6 +320,53 @@ def make_test_context( return context +_OPENSSL_ERR_LIB_SYS = 2 + + +def _get_openssl_error_lib(): + if ctypes is None: + raise unittest.SkipTest("ctypes is required") + + for candidate in ( + ctypes.util.find_library("crypto"), + ctypes.util.find_library("ssl"), + _ssl.__file__, + ): + if not candidate: + continue + try: + lib = ctypes.CDLL(candidate) + except OSError: + continue + if hasattr(lib, "ERR_clear_error") and hasattr(lib, "ERR_peek_last_error"): + lib.ERR_peek_last_error.restype = ctypes.c_ulong + return lib + raise unittest.SkipTest("OpenSSL error API not reachable via ctypes") + + +def _prime_openssl_sys_error_queue(lib, reason): + lib.ERR_clear_error() + if hasattr(lib, "ERR_new") and hasattr(lib, "ERR_set_debug") and hasattr(lib, "ERR_set_error"): + lib.ERR_set_debug.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p] + lib.ERR_set_error.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_char_p] + lib.ERR_new() + lib.ERR_set_debug(b"Lib/test/test_ssl.py", 0, + b"_prime_openssl_sys_error_queue") + lib.ERR_set_error(_OPENSSL_ERR_LIB_SYS, reason, b"") + return + if hasattr(lib, "ERR_put_error"): + lib.ERR_put_error.argtypes = [ + ctypes.c_int, ctypes.c_int, ctypes.c_int, + ctypes.c_char_p, ctypes.c_int, + ] + lib.ERR_put_error( + _OPENSSL_ERR_LIB_SYS, 0, reason, + b"Lib/test/test_ssl.py", 0, + ) + return + raise unittest.SkipTest("No supported OpenSSL error injection API") + + def test_wrap_socket( sock, *, @@ -2134,6 +2182,21 @@ def test_non_blocking_connect_ex(self): # SSL established self.assertTrue(s.getpeercert()) + @unittest.skipIf(ctypes is None, "requires ctypes") + def test_send_clears_stale_openssl_error_queue(self): + with test_wrap_socket(socket.socket(socket.AF_INET), + cert_reqs=ssl.CERT_REQUIRED, + ca_certs=SIGNING_CA) as s: + s.connect(self.server_addr) + + lib = _get_openssl_error_lib() + _prime_openssl_sys_error_queue(lib, errno.EPIPE) + self.assertNotEqual(lib.ERR_peek_last_error(), 0) + + self.assertEqual(s.send(b"x"), 1) + + self.assertEqual(lib.ERR_peek_last_error(), 0) + def test_connect_with_context(self): # Same as test_connect, but with a separately created context ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) diff --git a/Misc/NEWS.d/next/Library/2026-04-15-11-33-29.gh-issue-148594.4PdWRt.rst b/Misc/NEWS.d/next/Library/2026-04-15-11-33-29.gh-issue-148594.4PdWRt.rst new file mode 100644 index 00000000000000..2ce6bbf5beca8d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-15-11-33-29.gh-issue-148594.4PdWRt.rst @@ -0,0 +1,3 @@ +Fix stale OpenSSL per-thread error queue handling in +``ssl.SSLSocket.read()`` and ``ssl.SSLSocket.write()``. Patched by Shamil +Abdulaev. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 4e563379098eaf..7be770cfb1ef63 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2786,6 +2786,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; @@ -2938,6 +2939,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; From 32d0e37dd04376d2dec379af53076499600ad185 Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Wed, 15 Apr 2026 11:39:50 +0300 Subject: [PATCH 2/4] add recv test --- Lib/test/test_ssl.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 2a8f4de2089356..7768d1b57c698b 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -2197,6 +2197,24 @@ def test_send_clears_stale_openssl_error_queue(self): self.assertEqual(lib.ERR_peek_last_error(), 0) + @unittest.skipIf(ctypes is None, "requires ctypes") + def test_recv_clears_stale_openssl_error_queue(self): + with test_wrap_socket(socket.socket(socket.AF_INET), + cert_reqs=ssl.CERT_REQUIRED, + ca_certs=SIGNING_CA) as s: + s.connect(self.server_addr) + + s.send(b"x") + + lib = _get_openssl_error_lib() + _prime_openssl_sys_error_queue(lib, errno.EPIPE) + self.assertNotEqual(lib.ERR_peek_last_error(), 0) + + data = s.recv(1) + + self.assertEqual(data, b"x") + self.assertEqual(lib.ERR_peek_last_error(), 0) + def test_connect_with_context(self): # Same as test_connect, but with a separately created context ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) From 7c2bacf35e98cdbf36c3a2937f80a14967be257a Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Wed, 15 Apr 2026 11:47:49 +0300 Subject: [PATCH 3/4] fix tests --- Lib/test/test_ssl.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 7768d1b57c698b..06b91fcb4ddf69 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -2193,10 +2193,12 @@ def test_send_clears_stale_openssl_error_queue(self): _prime_openssl_sys_error_queue(lib, errno.EPIPE) self.assertNotEqual(lib.ERR_peek_last_error(), 0) + # Operation must succeed despite the stale error. + # We do not assert the queue is empty afterward because + # SSL_write_ex / SSL_get_error may legitimately post + # new entries (observed on AWS-LC). self.assertEqual(s.send(b"x"), 1) - self.assertEqual(lib.ERR_peek_last_error(), 0) - @unittest.skipIf(ctypes is None, "requires ctypes") def test_recv_clears_stale_openssl_error_queue(self): with test_wrap_socket(socket.socket(socket.AF_INET), @@ -2211,9 +2213,7 @@ def test_recv_clears_stale_openssl_error_queue(self): self.assertNotEqual(lib.ERR_peek_last_error(), 0) data = s.recv(1) - self.assertEqual(data, b"x") - self.assertEqual(lib.ERR_peek_last_error(), 0) def test_connect_with_context(self): # Same as test_connect, but with a separately created context From d1829c86e960187ef073a5ea5ec737506429cf49 Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Wed, 15 Apr 2026 12:02:17 +0300 Subject: [PATCH 4/4] Update test_ssl.py --- Lib/test/test_ssl.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 06b91fcb4ddf69..c74d91a9e05d14 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -327,10 +327,13 @@ def _get_openssl_error_lib(): if ctypes is None: raise unittest.SkipTest("ctypes is required") + # Try _ssl first: it is already loaded and linked to the correct + # OpenSSL/AWS-LC. Falling back to find_library() may locate a + # different system libcrypto and abort the process (macOS). for candidate in ( + _ssl.__file__, ctypes.util.find_library("crypto"), ctypes.util.find_library("ssl"), - _ssl.__file__, ): if not candidate: continue