Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from contextlib import nullcontext
try:
import ctypes
import ctypes.util
except ImportError:
ctypes = None

Expand Down Expand Up @@ -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,
*,
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix stale OpenSSL per-thread error queue handling in
``ssl.SSLSocket.read()`` and ``ssl.SSLSocket.write()``. Patched by Shamil
Abdulaev.
2 changes: 2 additions & 0 deletions Modules/_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading