diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 01b26bbf794764..c74d91a9e05d14 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,56 @@ 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") + + # 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"), + ): + 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 +2185,39 @@ 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) + + # 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) + + @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") + 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;