From 83ac22c6edd81007b372f017ff579742d4cfffc1 Mon Sep 17 00:00:00 2001 From: julianz- Date: Fri, 18 Aug 2023 12:25:50 -0700 Subject: [PATCH 01/44] Fix for problem caused by SSL_WANT_READ or SSL_WANT_WRITE errors. When SSL_WANT_READ or SSL_WANT_WRITE are encountered, it's typical to retry the call but this must be repeated with the exact same arguments. Without this change, openSSL requires that the address of the buffer passed is the same. However, buffers in python can change location in some circumstances which cause the retry to fail. By add the setting SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER, the requirement for the same buffer address is forgiven and the retry has a better chance of success. See cherrypy/cheroot#245 for discussion. --- CHANGELOG.rst | 15 +++++++++++++++ setup.py | 2 +- tox.ini | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d98901f3..5ae67749 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,21 @@ Changelog Versions are year-based with a strict backward-compatibility policy. The third digit is only for regressions. +25.2.0 (UNRELEASED) +------------------- +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +pyOpenSSL now sets SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER by default, matching CPython's behavior. #1287. +The minimum cryptography version is now 42.0.0. + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + + + 25.1.0 (2025-05-17) ------------------- diff --git a/setup.py b/setup.py index 676ecedc..dbb53099 100644 --- a/setup.py +++ b/setup.py @@ -94,7 +94,7 @@ def find_meta(meta): packages=find_packages(where="src"), package_dir={"": "src"}, install_requires=[ - "cryptography>=41.0.5,<46", + "cryptography>=42.0.0,<46", ( "typing-extensions>=4.9; " "python_version < '3.13' and python_version >= '3.8'" diff --git a/tox.ini b/tox.ini index 4a673e55..babaaed7 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ extras = test deps = coverage>=4.2 - cryptographyMinimum: cryptography==41.0.5 + cryptographyMinimum: cryptography==42.0.0 randomorder: pytest-randomly setenv = # Do not allow the executing environment to pollute the test environment From 2e8dbce6996a4a5eff8fbe3db10627cc86e352de Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 13 Jul 2025 20:14:36 -0700 Subject: [PATCH 02/44] Set SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER in calling OpenSSL --- CHANGELOG.rst | 1 - src/OpenSSL/SSL.py | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5ae67749..a41eed86 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,7 +18,6 @@ Changes: ^^^^^^^^ - 25.1.0 (2025-05-17) ------------------- diff --git a/src/OpenSSL/SSL.py b/src/OpenSSL/SSL.py index 81774c85..d3a953f0 100644 --- a/src/OpenSSL/SSL.py +++ b/src/OpenSSL/SSL.py @@ -915,7 +915,10 @@ def __init__(self, method: int) -> None: ) self._cookie_verify_helper: _CookieVerifyCallbackHelper | None = None - self.set_mode(_lib.SSL_MODE_ENABLE_PARTIAL_WRITE) + self.set_mode( + _lib.SSL_MODE_ENABLE_PARTIAL_WRITE + | _lib.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER + ) if version is not None: self.set_min_proto_version(version) self.set_max_proto_version(version) From 4c56afcf8c06b08bfd99150d7d268c9ad6b9a532 Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 14 Jul 2025 00:18:57 -0700 Subject: [PATCH 03/44] removed Python 3.7 from the -cryptographyMain tests because it fails --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11e5f9fd..a29d2ad1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: - {VERSION: "pypy-3.9", TOXENV: "pypy3-cryptographyMain"} - {VERSION: "pypy-3.10", TOXENV: "pypy3-cryptographyMain"} # -cryptographyMinimum - - {VERSION: "3.7", TOXENV: "py37-cryptographyMinimum"} + - {VERSION: "3.8", TOXENV: "py38-cryptographyMinimum"} - {VERSION: "3.9", TOXENV: "py39-cryptographyMinimum"} - {VERSION: "3.10", TOXENV: "py310-cryptographyMinimum"} From 9eab5f37f32634d7a393839eef53be16c14a01f5 Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 14 Jul 2025 00:24:02 -0700 Subject: [PATCH 04/44] removed failing tests that are no longer compatible --- .github/workflows/ci.yml | 2 +- tests/test_ssl.py | 30 ------------------------------ 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a29d2ad1..11e5f9fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: - {VERSION: "pypy-3.9", TOXENV: "pypy3-cryptographyMain"} - {VERSION: "pypy-3.10", TOXENV: "pypy3-cryptographyMain"} # -cryptographyMinimum - + - {VERSION: "3.7", TOXENV: "py37-cryptographyMinimum"} - {VERSION: "3.8", TOXENV: "py38-cryptographyMinimum"} - {VERSION: "3.9", TOXENV: "py39-cryptographyMinimum"} - {VERSION: "3.10", TOXENV: "py310-cryptographyMinimum"} diff --git a/tests/test_ssl.py b/tests/test_ssl.py index bcad6d96..ac15fd75 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3259,27 +3259,6 @@ def test_text(self) -> None: assert count == 2 assert client.recv(2) == b"xy" - def test_short_memoryview(self) -> None: - """ - When passed a memoryview onto a small number of bytes, - `Connection.send` transmits all of them and returns the number - of bytes sent. - """ - server, client = loopback() - count = server.send(memoryview(b"xy")) - assert count == 2 - assert client.recv(2) == b"xy" - - def test_short_bytearray(self) -> None: - """ - When passed a short bytearray, `Connection.send` transmits all of - it and returns the number of bytes sent. - """ - server, client = loopback() - count = server.send(bytearray(b"xy")) - assert count == 2 - assert client.recv(2) == b"xy" - @pytest.mark.skipif( sys.maxsize < 2**31, reason="sys.maxsize < 2**31 - test requires 64 bit", @@ -3472,15 +3451,6 @@ def test_text(self) -> None: ) == str(w[-1].message) assert client.recv(1) == b"x" - def test_short_memoryview(self) -> None: - """ - When passed a memoryview onto a small number of bytes, - `Connection.sendall` transmits all of them. - """ - server, client = loopback() - server.sendall(memoryview(b"x")) - assert client.recv(1) == b"x" - def test_long(self) -> None: """ `Connection.sendall` transmits all the bytes in the string passed to it From 3dde737e04ff6ee841fc67e246da7855f33180cd Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 14 Jul 2025 00:30:51 -0700 Subject: [PATCH 05/44] removed failing test --- tests/test_ssl.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index ac15fd75..f4fa33b3 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -2356,7 +2356,6 @@ def test_bio_write(self) -> None: context = Context(SSLv23_METHOD) connection = Connection(context, None) connection.bio_write(b"xy") - connection.bio_write(bytearray(b"za")) with pytest.warns(DeprecationWarning): connection.bio_write("deprecated") # type: ignore[arg-type] From 0437ba8f16a9ab573bed5b9079c89d91dac1f01c Mon Sep 17 00:00:00 2001 From: julianz- Date: Sat, 19 Jul 2025 21:27:17 -0700 Subject: [PATCH 06/44] Revert "removed failing tests that are no longer compatible" This reverts commit dee294a2fe0a57b98738605c3e88e8b69b287290. --- .github/workflows/ci.yml | 2 +- tests/test_ssl.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11e5f9fd..a29d2ad1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: - {VERSION: "pypy-3.9", TOXENV: "pypy3-cryptographyMain"} - {VERSION: "pypy-3.10", TOXENV: "pypy3-cryptographyMain"} # -cryptographyMinimum - - {VERSION: "3.7", TOXENV: "py37-cryptographyMinimum"} + - {VERSION: "3.8", TOXENV: "py38-cryptographyMinimum"} - {VERSION: "3.9", TOXENV: "py39-cryptographyMinimum"} - {VERSION: "3.10", TOXENV: "py310-cryptographyMinimum"} diff --git a/tests/test_ssl.py b/tests/test_ssl.py index f4fa33b3..bd8bd9e9 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3258,6 +3258,27 @@ def test_text(self) -> None: assert count == 2 assert client.recv(2) == b"xy" + def test_short_memoryview(self) -> None: + """ + When passed a memoryview onto a small number of bytes, + `Connection.send` transmits all of them and returns the number + of bytes sent. + """ + server, client = loopback() + count = server.send(memoryview(b"xy")) + assert count == 2 + assert client.recv(2) == b"xy" + + def test_short_bytearray(self) -> None: + """ + When passed a short bytearray, `Connection.send` transmits all of + it and returns the number of bytes sent. + """ + server, client = loopback() + count = server.send(bytearray(b"xy")) + assert count == 2 + assert client.recv(2) == b"xy" + @pytest.mark.skipif( sys.maxsize < 2**31, reason="sys.maxsize < 2**31 - test requires 64 bit", @@ -3450,6 +3471,15 @@ def test_text(self) -> None: ) == str(w[-1].message) assert client.recv(1) == b"x" + def test_short_memoryview(self) -> None: + """ + When passed a memoryview onto a small number of bytes, + `Connection.sendall` transmits all of them. + """ + server, client = loopback() + server.sendall(memoryview(b"x")) + assert client.recv(1) == b"x" + def test_long(self) -> None: """ `Connection.sendall` transmits all the bytes in the string passed to it From b82933b621a47f4717547a0b8313039bce4007a7 Mon Sep 17 00:00:00 2001 From: julianz- Date: Sat, 19 Jul 2025 21:28:48 -0700 Subject: [PATCH 07/44] Revert "removed failing test" This reverts commit 339b193c73ab4b88cff287bdb23c1974b41fe7f3. --- tests/test_ssl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index bd8bd9e9..bcad6d96 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -2356,6 +2356,7 @@ def test_bio_write(self) -> None: context = Context(SSLv23_METHOD) connection = Connection(context, None) connection.bio_write(b"xy") + connection.bio_write(bytearray(b"za")) with pytest.warns(DeprecationWarning): connection.bio_write("deprecated") # type: ignore[arg-type] From 4a463cb162bd47022da4986dabfa36915e4e5657 Mon Sep 17 00:00:00 2001 From: julianz- Date: Sat, 19 Jul 2025 23:13:43 -0700 Subject: [PATCH 08/44] tests added for checking SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER --- src/OpenSSL/SSL.py | 10 ++ tests/test_ssl.py | 225 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+) diff --git a/src/OpenSSL/SSL.py b/src/OpenSSL/SSL.py index d3a953f0..88b08e5b 100644 --- a/src/OpenSSL/SSL.py +++ b/src/OpenSSL/SSL.py @@ -1716,6 +1716,16 @@ def set_mode(self, mode: int) -> int: raise TypeError("mode must be an integer") return _lib.SSL_CTX_set_mode(self._context, mode) + + #@_require_not_used + def clear_mode(self) -> int: + """ + Modes previously set cannot be overwritten without being cleared first. + This method should be used to clear prior to re-settting. + """ + + mode = 0xFFFFFFFF + return _lib.SSL_CTX_clear_mode(self._context, mode) @_require_not_used def set_tlsext_servername_callback( diff --git a/tests/test_ssl.py b/tests/test_ssl.py index bcad6d96..16b55afc 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -31,6 +31,9 @@ AF_INET6, MSG_PEEK, SHUT_RDWR, + SOL_SOCKET, + SO_SNDBUF, + SO_RCVBUF, gaierror, socket, ) @@ -413,6 +416,119 @@ def handshake_in_memory( interact_in_memory(client_conn, server_conn) +def get_ssl_error_reason(ssl_error: SSL.Error) -> typing.Optional[str]: + """ + Extracts the reason string from the first error tuple in an OpenSSL.SSL.Error. + Returns None if the expected error structure is not found. + """ + if ssl_error.args and isinstance(ssl_error.args, tuple) and len(ssl_error.args) > 0: + error_details = ssl_error.args[0] # This should be the list of error tuples + if isinstance(error_details, list) and len(error_details) > 0: + first_error_tuple = error_details[0] + if isinstance(first_error_tuple, tuple) and len(first_error_tuple) >= 3: + return first_error_tuple[2] + return None + +def create_ssl_nonblocking_connection(mode: int) -> tuple[socket, socket, Connection, Connection]: + """ + Create a pair of sockets and set up an SSL connection between them. + """ + # Create a private key and a certificate to use for the server + key = PKey() + key.generate_key(TYPE_RSA, 2048) + cert = X509() + cert.set_version(2) + cert.get_subject().C = b"US" + cert.get_subject().ST = b"California" + cert.get_subject().L = b"Palo Alto" + cert.get_subject().O = b"pyOpenSSL" + cert.get_subject().CN = b"localhost" + cert.set_serial_number(1) + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(60 * 60) + cert.set_issuer(cert.get_subject()) + cert.set_pubkey(key) + cert.sign(key, "sha1") + + # Create a context with the necessary modes + ctx = Context(SSLv23_METHOD) + + ctx.clear_mode() + ctx.set_mode(mode) + ctx.use_privatekey(key) + ctx.use_certificate(cert) + + # Create connections with real sockets + client_socket, server_socket = socket_pair() + + # Create Connection objects from the sockets + client = Connection(ctx, client_socket) + server = Connection(ctx, server_socket) + + # Set the buffers to be very small so we can easily fill them + client_socket.setsockopt(SOL_SOCKET, SO_SNDBUF, 256) + server_socket.setsockopt(SOL_SOCKET, SO_RCVBUF, 128) + + # Manually set the connection state + client.set_connect_state() + server.set_accept_state() + + # Perform the handshake with proper completion detection + client_handshake_done = False + server_handshake_done = False + max_handshake_attempts = 100 # Prevent infinite loops + attempts = 0 + + while not (client_handshake_done and server_handshake_done) and attempts < max_handshake_attempts: + attempts += 1 + + # Try client handshake + if not client_handshake_done: + try: + client.do_handshake() + client_handshake_done = True + except SSL.WantReadError: + # Client needs to read data + pass + except SSL.WantWriteError: + # Client needs to write data + pass + + # Try server handshake + if not server_handshake_done: + try: + server.do_handshake() + server_handshake_done = True + except SSL.WantReadError: + # Server needs to read data + pass + except SSL.WantWriteError: + # Server needs to write data + pass + + # If neither handshake is complete, wait for socket activity + if not (client_handshake_done and server_handshake_done): + # Use select to wait for socket activity + ready_read, ready_write, ready_error = select.select( + [client_socket, server_socket], + [client_socket, server_socket], + [client_socket, server_socket], + 1.0 # 1 second timeout + ) + + if ready_error: + raise Exception(f"Socket error during handshake: {ready_error}") + + if not (ready_read or ready_write): + # Timeout occurred, but continue trying + continue + + if not (client_handshake_done and server_handshake_done): + raise Exception("SSL handshake failed to complete") + return client_socket,server_socket,client,server + + + class TestVersion: """ Tests for version information exposed by `OpenSSL.SSL.SSLeay_version` and @@ -2997,6 +3113,115 @@ def test_wantWriteError(self) -> None: # XXX want_read + def _badwriteretry(self, mode) -> bool: + """ + `Connection` methods which generate output raise + `OpenSSL.SSL.WantWriteError` if writing to the connection's BIO + fail indicating a should-write state. + """ + client_socket, server_socket, client, server = create_ssl_nonblocking_connection(mode) + + written = 0 + + import os + print("Client PID:") + print(os.getpid()) + + # Fill up the client's raw send buffer so the SSL connection won't be able to write + # anything. Start by sending larger chunks + # and continue by writing smaller chunks so we can be sure we + # completely fill the buffer. + msg = 'test' + for msg in [b"x" * 65536, b"x" * 16]: + for i in range(1024 * 1024 * 64): + try: + written = client_socket.send(msg) + print(f"Sent {written} bytes to fill buffer") + except OSError as e: + if e.errno == EWOULDBLOCK: + break + raise + else: + pytest.fail( + "Failed to fill socket buffer, cannot test bad write error" + ) + + # Now, attempt to send application data over the *established* SSL connection. + # Since the underlying raw socket's buffer is full, this should cause a WantWriteError. + print("Attempting to send data over SSL connection with full buffer...") + msg2 = b"Y" * 65536 #b"This data should trigger WantWriteError due to full buffer." + + try: + written = client.send(msg2) + except SSL.WantWriteError as e: + print(f"Raised OpenSSL.SSL.WantWriteError as expected: {e} {written} bytes") + try: + # do a retry write which should fail unless SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER is set + # because we are passing a new buffer that has the same size as the previous but a different location + msg3 = b"Z" * 65536 + written = client.send(msg3) + print(f"Retry succeeded unexpectedly: {written} bytes") + except SSL.Error as e: + reason = get_ssl_error_reason(e) + if reason == "bad write retry": + print(f"Got expected SSL error on retry: {e}") + return True + else: + print(f"Got unexpected SSL error on retry: {e}") + return False + #print(f"Got expected SSL error on retry: {e} {written} bytes") + + except SSL.Error as e: + reason = get_ssl_error_reason(e) + pytest.fail(f"Got unexpected SSL error on retry: {e} {reason}") + except Exception as e: + pytest.fail(f"Unexpected exception during send: {e}") + + finally: + # Cleanup: shut down SSL connections and close raw sockets + try: + if client: + client.shutdown() + if server: + server.shutdown() + except Exception as e: + print(f"Error during SSL shutdown: {e}") + finally: + if client_socket: + client_socket.close() + if server_socket: + server_socket.close() + + def test_moving_write_buffer_should_pass(self) -> None: + """ + After an `OpenSSL.SSL.WantWriteError` if the SSL connection processed some data + the connection may expect a retry with the same buffer. Using mode SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER makes + possible to use a different buffer location provided the length is the same. + """ + mode = ( + _lib.SSL_MODE_ENABLE_PARTIAL_WRITE + | _lib.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER + ) + result = self._badwriteretry(mode) + + if result: + pytest.fail("Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to prevent bad write rety") + + def test_moving_write_buffer_should_fail(self) -> None: + """ + After an `OpenSSL.SSL.WantWriteError` if the SSL connection processed some data + the connection may expect a retry with the same buffer. Failure to use mode SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER + should generate a bad write retry error if a different buffer is presented for the retry. + """ + mode = 0 + result = self._badwriteretry(mode) + + if not result: + pytest.fail("Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to prevent bad write rety") + + # XXX want_read + + def test_get_finished_before_connect(self) -> None: """ `Connection.get_finished` returns `None` before TLS handshake From 3f581dc3f924ef3a2cb5dd50e1019c31710d3e22 Mon Sep 17 00:00:00 2001 From: julianz- Date: Sat, 19 Jul 2025 23:49:14 -0700 Subject: [PATCH 09/44] added type fixes to tests --- tests/test_ssl.py | 114 +++++++++++++++++++++++----------------------- 1 file changed, 56 insertions(+), 58 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 16b55afc..203c8bb4 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -426,7 +426,9 @@ def get_ssl_error_reason(ssl_error: SSL.Error) -> typing.Optional[str]: if isinstance(error_details, list) and len(error_details) > 0: first_error_tuple = error_details[0] if isinstance(first_error_tuple, tuple) and len(first_error_tuple) >= 3: - return first_error_tuple[2] + reason = first_error_tuple[2] + if isinstance(reason, str): + return reason return None def create_ssl_nonblocking_connection(mode: int) -> tuple[socket, socket, Connection, Connection]: @@ -2472,7 +2474,7 @@ def test_bio_write(self) -> None: context = Context(SSLv23_METHOD) connection = Connection(context, None) connection.bio_write(b"xy") - connection.bio_write(bytearray(b"za")) + connection.bio_write(bytearray(b"za")) # type: ignore[arg-type] with pytest.warns(DeprecationWarning): connection.bio_write("deprecated") # type: ignore[arg-type] @@ -3113,69 +3115,63 @@ def test_wantWriteError(self) -> None: # XXX want_read - def _badwriteretry(self, mode) -> bool: + def _badwriteretry(self, mode: int) -> bool: """ - `Connection` methods which generate output raise - `OpenSSL.SSL.WantWriteError` if writing to the connection's BIO - fail indicating a should-write state. + Tries to force a "bad write retry" error over an SSL connection by using a moving buffer + Returns True if a bad write retry error occurs. """ client_socket, server_socket, client, server = create_ssl_nonblocking_connection(mode) - + result = False # Default return value written = 0 - import os - print("Client PID:") - print(os.getpid()) - - # Fill up the client's raw send buffer so the SSL connection won't be able to write - # anything. Start by sending larger chunks - # and continue by writing smaller chunks so we can be sure we - # completely fill the buffer. - msg = 'test' - for msg in [b"x" * 65536, b"x" * 16]: - for i in range(1024 * 1024 * 64): - try: - written = client_socket.send(msg) - print(f"Sent {written} bytes to fill buffer") - except OSError as e: - if e.errno == EWOULDBLOCK: - break - raise - else: - pytest.fail( - "Failed to fill socket buffer, cannot test bad write error" - ) + try: + # Fill up the client's raw send buffer so the SSL connection won't be able to write + # anything. Start by sending larger chunks + # and continue by writing smaller chunks so we can be sure we + # completely fill the buffer. + for msg in [b"x" * 65536, b"x" * 16]: + for i in range(1024 * 1024 * 64): + try: + written = client_socket.send(msg) + print(f"Sent {written} bytes to fill buffer") + except OSError as e: + if e.errno == EWOULDBLOCK: + break + raise + else: + pytest.fail( + "Failed to fill socket buffer, cannot test bad write error" + ) - # Now, attempt to send application data over the *established* SSL connection. - # Since the underlying raw socket's buffer is full, this should cause a WantWriteError. - print("Attempting to send data over SSL connection with full buffer...") - msg2 = b"Y" * 65536 #b"This data should trigger WantWriteError due to full buffer." + # Now, attempt to send application data over the *established* SSL connection. + # Since the underlying raw socket's buffer is full, this should cause a WantWriteError. + print("Attempting to send data over SSL connection with full buffer...") + msg2 = b"Y" * 65536 - try: - written = client.send(msg2) - except SSL.WantWriteError as e: - print(f"Raised OpenSSL.SSL.WantWriteError as expected: {e} {written} bytes") try: - # do a retry write which should fail unless SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER is set - # because we are passing a new buffer that has the same size as the previous but a different location - msg3 = b"Z" * 65536 - written = client.send(msg3) - print(f"Retry succeeded unexpectedly: {written} bytes") + written = client.send(msg2) + except SSL.WantWriteError as e: + print(f"Raised OpenSSL.SSL.WantWriteError as expected: {e} {written} bytes") + try: + # do a retry write which should fail unless SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER is set + msg3 = b"Z" * 65536 + written = client.send(msg3) + print(f"Retry succeeded unexpectedly: {written} bytes") + result = False # Unexpected success + except SSL.Error as e: + reason = get_ssl_error_reason(e) + if reason == "bad write retry": + print(f"Got expected SSL error on retry: {e}") + result = True # Expected behavior + else: + print(f"Got unexpected SSL error on retry: {e}") + result = False # Unexpected error + except SSL.Error as e: reason = get_ssl_error_reason(e) - if reason == "bad write retry": - print(f"Got expected SSL error on retry: {e}") - return True - else: - print(f"Got unexpected SSL error on retry: {e}") - return False - #print(f"Got expected SSL error on retry: {e} {written} bytes") - - except SSL.Error as e: - reason = get_ssl_error_reason(e) - pytest.fail(f"Got unexpected SSL error on retry: {e} {reason}") - except Exception as e: - pytest.fail(f"Unexpected exception during send: {e}") + pytest.fail(f"Got unexpected SSL error on retry: {e} {reason}") + except Exception as e: + pytest.fail(f"Unexpected exception during send: {e}") finally: # Cleanup: shut down SSL connections and close raw sockets @@ -3191,6 +3187,8 @@ def _badwriteretry(self, mode) -> bool: client_socket.close() if server_socket: server_socket.close() + + return result # Return the result after cleanup def test_moving_write_buffer_should_pass(self) -> None: """ @@ -3491,7 +3489,7 @@ def test_short_memoryview(self) -> None: of bytes sent. """ server, client = loopback() - count = server.send(memoryview(b"xy")) + count = server.send(memoryview(b"xy")) # type: ignore[arg-type] assert count == 2 assert client.recv(2) == b"xy" @@ -3501,7 +3499,7 @@ def test_short_bytearray(self) -> None: it and returns the number of bytes sent. """ server, client = loopback() - count = server.send(bytearray(b"xy")) + count = server.send(bytearray(b"xy")) # type: ignore[arg-type] assert count == 2 assert client.recv(2) == b"xy" @@ -3703,7 +3701,7 @@ def test_short_memoryview(self) -> None: `Connection.sendall` transmits all of them. """ server, client = loopback() - server.sendall(memoryview(b"x")) + server.sendall(memoryview(b"x")) # type: ignore[arg-type] assert client.recv(1) == b"x" def test_long(self) -> None: From 9246994dea2eeae4922bb08982e14567c9b80a5e Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 20 Jul 2025 00:27:13 -0700 Subject: [PATCH 10/44] fixed clear_mode --- src/OpenSSL/SSL.py | 8 +++----- tests/test_ssl.py | 12 ++++++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/OpenSSL/SSL.py b/src/OpenSSL/SSL.py index 88b08e5b..7dddd3eb 100644 --- a/src/OpenSSL/SSL.py +++ b/src/OpenSSL/SSL.py @@ -1718,14 +1718,12 @@ def set_mode(self, mode: int) -> int: return _lib.SSL_CTX_set_mode(self._context, mode) #@_require_not_used - def clear_mode(self) -> int: + def clear_mode(self, mode_to_clear: int) -> int: """ Modes previously set cannot be overwritten without being cleared first. - This method should be used to clear prior to re-settting. + This method should be used to clear existing modes """ - - mode = 0xFFFFFFFF - return _lib.SSL_CTX_clear_mode(self._context, mode) + return _lib.SSL_CTX_clear_mode(self._context, mode_to_clear) @_require_not_used def set_tlsext_servername_callback( diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 203c8bb4..37d3b457 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -455,7 +455,11 @@ def create_ssl_nonblocking_connection(mode: int) -> tuple[socket, socket, Connec # Create a context with the necessary modes ctx = Context(SSLv23_METHOD) - ctx.clear_mode() + # these modes are set by default when ctx is initialized + # clear them so we can run tests with or without them + ctx.clear_mode(_lib.SSL_MODE_ENABLE_PARTIAL_WRITE + | _lib.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER) + ctx.set_mode(mode) ctx.use_privatekey(key) ctx.use_certificate(cert) @@ -3489,7 +3493,7 @@ def test_short_memoryview(self) -> None: of bytes sent. """ server, client = loopback() - count = server.send(memoryview(b"xy")) # type: ignore[arg-type] + count = server.send(memoryview(b"xy")) assert count == 2 assert client.recv(2) == b"xy" @@ -3499,7 +3503,7 @@ def test_short_bytearray(self) -> None: it and returns the number of bytes sent. """ server, client = loopback() - count = server.send(bytearray(b"xy")) # type: ignore[arg-type] + count = server.send(bytearray(b"xy")) assert count == 2 assert client.recv(2) == b"xy" @@ -3701,7 +3705,7 @@ def test_short_memoryview(self) -> None: `Connection.sendall` transmits all of them. """ server, client = loopback() - server.sendall(memoryview(b"x")) # type: ignore[arg-type] + server.sendall(memoryview(b"x")) assert client.recv(1) == b"x" def test_long(self) -> None: From bddb04de5770b05efee92d14f09a3ce8fb73fecf Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 20 Jul 2025 09:33:23 -0700 Subject: [PATCH 11/44] cleaned up for flake8 --- tests/test_ssl.py | 297 ++++++++++++++++++++++++---------------------- 1 file changed, 156 insertions(+), 141 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 37d3b457..86dfd38a 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -417,122 +417,128 @@ def handshake_in_memory( def get_ssl_error_reason(ssl_error: SSL.Error) -> typing.Optional[str]: - """ - Extracts the reason string from the first error tuple in an OpenSSL.SSL.Error. - Returns None if the expected error structure is not found. - """ - if ssl_error.args and isinstance(ssl_error.args, tuple) and len(ssl_error.args) > 0: - error_details = ssl_error.args[0] # This should be the list of error tuples - if isinstance(error_details, list) and len(error_details) > 0: - first_error_tuple = error_details[0] - if isinstance(first_error_tuple, tuple) and len(first_error_tuple) >= 3: - reason = first_error_tuple[2] - if isinstance(reason, str): - return reason - return None + """ + Extracts the reason string from the first error tuple in an SSL.Error. + Returns None if the expected error structure is not found. + """ + if ssl_error.args and isinstance(ssl_error.args, tuple) and \ + len(ssl_error.args) > 0: + error_details = ssl_error.args[0] # list of error tuples + if isinstance(error_details, list) and len(error_details) > 0: + first_error_tuple = error_details[0] + if isinstance(first_error_tuple, tuple) and \ + len(first_error_tuple) >= 3: + reason = first_error_tuple[2] + if isinstance(reason, str): + return reason + return None -def create_ssl_nonblocking_connection(mode: int) -> tuple[socket, socket, Connection, Connection]: - """ - Create a pair of sockets and set up an SSL connection between them. - """ - # Create a private key and a certificate to use for the server - key = PKey() - key.generate_key(TYPE_RSA, 2048) - cert = X509() - cert.set_version(2) - cert.get_subject().C = b"US" - cert.get_subject().ST = b"California" - cert.get_subject().L = b"Palo Alto" - cert.get_subject().O = b"pyOpenSSL" - cert.get_subject().CN = b"localhost" - cert.set_serial_number(1) - cert.gmtime_adj_notBefore(0) - cert.gmtime_adj_notAfter(60 * 60) - cert.set_issuer(cert.get_subject()) - cert.set_pubkey(key) - cert.sign(key, "sha1") - - # Create a context with the necessary modes - ctx = Context(SSLv23_METHOD) - - # these modes are set by default when ctx is initialized - # clear them so we can run tests with or without them - ctx.clear_mode(_lib.SSL_MODE_ENABLE_PARTIAL_WRITE - | _lib.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER) - - ctx.set_mode(mode) - ctx.use_privatekey(key) - ctx.use_certificate(cert) - # Create connections with real sockets - client_socket, server_socket = socket_pair() +def create_ssl_nonblocking_connection(mode: int) \ + -> tuple[socket, socket, Connection, Connection]: + """ + Create a pair of sockets and set up an SSL connection between them. + """ + # Create a private key and a certificate to use for the server + key = PKey() + key.generate_key(TYPE_RSA, 2048) + cert = X509() + cert.set_version(2) + cert.get_subject().C = b"US" + cert.get_subject().ST = b"California" + cert.get_subject().L = b"Palo Alto" + cert.get_subject().O = b"pyOpenSSL" # noqa: E741 + cert.get_subject().CN = b"localhost" + cert.set_serial_number(1) + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(60 * 60) + cert.set_issuer(cert.get_subject()) + cert.set_pubkey(key) + cert.sign(key, "sha1") + + # Create a context with the necessary modes + ctx = Context(SSLv23_METHOD) - # Create Connection objects from the sockets - client = Connection(ctx, client_socket) - server = Connection(ctx, server_socket) + # these modes are set by default when ctx is initialized + # clear them so we can run tests with or without them + ctx.clear_mode( + _lib.SSL_MODE_ENABLE_PARTIAL_WRITE | + _lib.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER + ) - # Set the buffers to be very small so we can easily fill them - client_socket.setsockopt(SOL_SOCKET, SO_SNDBUF, 256) - server_socket.setsockopt(SOL_SOCKET, SO_RCVBUF, 128) + ctx.set_mode(mode) + ctx.use_privatekey(key) + ctx.use_certificate(cert) - # Manually set the connection state - client.set_connect_state() - server.set_accept_state() + # Create connections with real sockets + client_socket, server_socket = socket_pair() - # Perform the handshake with proper completion detection - client_handshake_done = False - server_handshake_done = False - max_handshake_attempts = 100 # Prevent infinite loops - attempts = 0 - - while not (client_handshake_done and server_handshake_done) and attempts < max_handshake_attempts: - attempts += 1 - - # Try client handshake - if not client_handshake_done: - try: - client.do_handshake() - client_handshake_done = True - except SSL.WantReadError: - # Client needs to read data - pass - except SSL.WantWriteError: - # Client needs to write data - pass - - # Try server handshake - if not server_handshake_done: - try: - server.do_handshake() - server_handshake_done = True - except SSL.WantReadError: - # Server needs to read data - pass - except SSL.WantWriteError: - # Server needs to write data - pass - - # If neither handshake is complete, wait for socket activity - if not (client_handshake_done and server_handshake_done): - # Use select to wait for socket activity - ready_read, ready_write, ready_error = select.select( - [client_socket, server_socket], - [client_socket, server_socket], - [client_socket, server_socket], - 1.0 # 1 second timeout - ) - - if ready_error: - raise Exception(f"Socket error during handshake: {ready_error}") - - if not (ready_read or ready_write): - # Timeout occurred, but continue trying - continue - + # Create Connection objects from the sockets + client = Connection(ctx, client_socket) + server = Connection(ctx, server_socket) + + # Set the buffers to be very small so we can easily fill them + client_socket.setsockopt(SOL_SOCKET, SO_SNDBUF, 256) + server_socket.setsockopt(SOL_SOCKET, SO_RCVBUF, 128) + + # Manually set the connection state + client.set_connect_state() + server.set_accept_state() + + # Perform the handshake with proper completion detection + client_handshake_done = False + server_handshake_done = False + max_handshake_attempts = 100 # Prevent infinite loops + attempts = 0 + while ( + not (client_handshake_done and server_handshake_done) + and attempts < max_handshake_attempts + ): + attempts += 1 + # Try client handshake + if not client_handshake_done: + try: + client.do_handshake() + client_handshake_done = True + except SSL.WantReadError: + # Client needs to read data + pass + except SSL.WantWriteError: + # Client needs to write data + pass + + # Try server handshake + if not server_handshake_done: + try: + server.do_handshake() + server_handshake_done = True + except SSL.WantReadError: + # Server needs to read data + pass + except SSL.WantWriteError: + # Server needs to write data + pass + + # If neither handshake is complete, wait for socket activity if not (client_handshake_done and server_handshake_done): - raise Exception("SSL handshake failed to complete") - return client_socket,server_socket,client,server - + # Use select to wait for socket activity + ready_read, ready_write, ready_err = select.select( + [client_socket, server_socket], + [client_socket, server_socket], + [client_socket, server_socket], + 1.0 # 1 second timeout + ) + + if ready_err: + raise Exception(f"Socket error during handshake: {ready_err}") + + if not (ready_read or ready_write): + # Timeout occurred, but continue trying + continue + + if not (client_handshake_done and server_handshake_done): + raise Exception("SSL handshake failed to complete") + return client_socket, server_socket, client, server class TestVersion: @@ -3121,18 +3127,20 @@ def test_wantWriteError(self) -> None: def _badwriteretry(self, mode: int) -> bool: """ - Tries to force a "bad write retry" error over an SSL connection by using a moving buffer - Returns True if a bad write retry error occurs. + Tries to force a "bad write retry" error over an SSL connection + by using a moving buffer. Returns True if a bad write retry + error occurs. """ - client_socket, server_socket, client, server = create_ssl_nonblocking_connection(mode) + client_socket, server_socket, client, server \ + = create_ssl_nonblocking_connection(mode) result = False # Default return value written = 0 try: - # Fill up the client's raw send buffer so the SSL connection won't be able to write - # anything. Start by sending larger chunks + # Fill up the client's raw send buffer so the SSL connection + # won't be able to write anything. Start by sending larger chunks # and continue by writing smaller chunks so we can be sure we - # completely fill the buffer. + # completely fill the buffer. for msg in [b"x" * 65536, b"x" * 16]: for i in range(1024 * 1024 * 64): try: @@ -3144,32 +3152,37 @@ def _badwriteretry(self, mode: int) -> bool: raise else: pytest.fail( - "Failed to fill socket buffer, cannot test bad write error" + "Failed to fill socket buffer, cannot test \ + bad write error" ) - # Now, attempt to send application data over the *established* SSL connection. - # Since the underlying raw socket's buffer is full, this should cause a WantWriteError. - print("Attempting to send data over SSL connection with full buffer...") + # Now, attempt to send application data over the established + # SSL connection. Since the underlying raw socket's buffer is full, + # this should cause a WantWriteError. msg2 = b"Y" * 65536 try: written = client.send(msg2) - except SSL.WantWriteError as e: - print(f"Raised OpenSSL.SSL.WantWriteError as expected: {e} {written} bytes") + except SSL.WantWriteError: try: - # do a retry write which should fail unless SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER is set + # After a WantWriteError if the connection has partially + # written the last buffer it will expect a retry write. + # This next write should fail but for two different reasons + # depending on whether SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER + # was set msg3 = b"Z" * 65536 written = client.send(msg3) - print(f"Retry succeeded unexpectedly: {written} bytes") - result = False # Unexpected success + pytest.fail("Retry succeeded unexpectedly") except SSL.Error as e: reason = get_ssl_error_reason(e) - if reason == "bad write retry": - print(f"Got expected SSL error on retry: {e}") - result = True # Expected behavior + if reason == "Bad write retry": + # Got SSL error on retry (expected if not using \ + # SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER) + result = True else: - print(f"Got unexpected SSL error on retry: {e}") - result = False # Unexpected error + # when using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER + # we expect this to fail for a WantWriteError + result = False except SSL.Error as e: reason = get_ssl_error_reason(e) @@ -3191,14 +3204,15 @@ def _badwriteretry(self, mode: int) -> bool: client_socket.close() if server_socket: server_socket.close() - + return result # Return the result after cleanup def test_moving_write_buffer_should_pass(self) -> None: """ - After an `OpenSSL.SSL.WantWriteError` if the SSL connection processed some data - the connection may expect a retry with the same buffer. Using mode SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER makes - possible to use a different buffer location provided the length is the same. + After an `OpenSSL.SSL.WantWriteError` if the SSL connection processed + some data, the connection may expect a retry with the same buffer. + Using mode SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER makes it possible + to use a different buffer location provided the length is the same. """ mode = ( _lib.SSL_MODE_ENABLE_PARTIAL_WRITE @@ -3207,22 +3221,23 @@ def test_moving_write_buffer_should_pass(self) -> None: result = self._badwriteretry(mode) if result: - pytest.fail("Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to prevent bad write rety") + pytest.fail("Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to \ + prevent bad write rety") def test_moving_write_buffer_should_fail(self) -> None: """ - After an `OpenSSL.SSL.WantWriteError` if the SSL connection processed some data - the connection may expect a retry with the same buffer. Failure to use mode SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER - should generate a bad write retry error if a different buffer is presented for the retry. + After an `OpenSSL.SSL.WantWriteError` if the SSL connection processed + some data, the connection may expect a retry with the same buffer. + Failure to use mode SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER + should generate a bad write retry error if a different + buffer is presented. """ mode = 0 result = self._badwriteretry(mode) if not result: - pytest.fail("Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to prevent bad write rety") - - # XXX want_read - + pytest.fail("Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to \ + prevent bad write rety") def test_get_finished_before_connect(self) -> None: """ From bbe8156444b7758698666c72f2a7cf7ed0ac2fa5 Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 20 Jul 2025 09:53:28 -0700 Subject: [PATCH 12/44] fixed test_moving_write_buffer_should_fail test --- tests/test_ssl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 86dfd38a..630ae12f 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3175,7 +3175,7 @@ def _badwriteretry(self, mode: int) -> bool: pytest.fail("Retry succeeded unexpectedly") except SSL.Error as e: reason = get_ssl_error_reason(e) - if reason == "Bad write retry": + if reason == "bad write retry": # Got SSL error on retry (expected if not using \ # SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER) result = True From 0dfc149e088b29742b741784f0cb52fcd4bf6c22 Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 20 Jul 2025 10:17:22 -0700 Subject: [PATCH 13/44] more lint fixes --- src/OpenSSL/SSL.py | 6 +++--- tests/test_ssl.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/OpenSSL/SSL.py b/src/OpenSSL/SSL.py index 7dddd3eb..254d8720 100644 --- a/src/OpenSSL/SSL.py +++ b/src/OpenSSL/SSL.py @@ -1716,12 +1716,12 @@ def set_mode(self, mode: int) -> int: raise TypeError("mode must be an integer") return _lib.SSL_CTX_set_mode(self._context, mode) - + #@_require_not_used def clear_mode(self, mode_to_clear: int) -> int: """ - Modes previously set cannot be overwritten without being cleared first. - This method should be used to clear existing modes + Modes previously set cannot be overwritten without being + cleared first. This method should be used to clear existing modes. """ return _lib.SSL_CTX_clear_mode(self._context, mode_to_clear) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 630ae12f..d299d4db 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -416,7 +416,7 @@ def handshake_in_memory( interact_in_memory(client_conn, server_conn) -def get_ssl_error_reason(ssl_error: SSL.Error) -> typing.Optional[str]: +def get_ssl_error_reason(ssl_error: SSL.Error) -> str | None: """ Extracts the reason string from the first error tuple in an SSL.Error. Returns None if the expected error structure is not found. @@ -447,7 +447,7 @@ def create_ssl_nonblocking_connection(mode: int) \ cert.get_subject().C = b"US" cert.get_subject().ST = b"California" cert.get_subject().L = b"Palo Alto" - cert.get_subject().O = b"pyOpenSSL" # noqa: E741 + cert.get_subject().O = b"pyOpenSSL" cert.get_subject().CN = b"localhost" cert.set_serial_number(1) cert.gmtime_adj_notBefore(0) @@ -2484,7 +2484,7 @@ def test_bio_write(self) -> None: context = Context(SSLv23_METHOD) connection = Connection(context, None) connection.bio_write(b"xy") - connection.bio_write(bytearray(b"za")) # type: ignore[arg-type] + connection.bio_write(bytearray(b"za")) with pytest.warns(DeprecationWarning): connection.bio_write("deprecated") # type: ignore[arg-type] From 57af3af3083897ce7ba875a50279463622b50bb5 Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 20 Jul 2025 10:30:03 -0700 Subject: [PATCH 14/44] formatting fixes --- tests/test_ssl.py | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index d299d4db..53b9f228 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -31,9 +31,9 @@ AF_INET6, MSG_PEEK, SHUT_RDWR, - SOL_SOCKET, - SO_SNDBUF, SO_RCVBUF, + SO_SNDBUF, + SOL_SOCKET, gaierror, socket, ) @@ -421,21 +421,27 @@ def get_ssl_error_reason(ssl_error: SSL.Error) -> str | None: Extracts the reason string from the first error tuple in an SSL.Error. Returns None if the expected error structure is not found. """ - if ssl_error.args and isinstance(ssl_error.args, tuple) and \ - len(ssl_error.args) > 0: + if ( + ssl_error.args + and isinstance(ssl_error.args, tuple) + and len(ssl_error.args) > 0 + ): error_details = ssl_error.args[0] # list of error tuples if isinstance(error_details, list) and len(error_details) > 0: first_error_tuple = error_details[0] - if isinstance(first_error_tuple, tuple) and \ - len(first_error_tuple) >= 3: + if ( + isinstance(first_error_tuple, tuple) + and len(first_error_tuple) >= 3 + ): reason = first_error_tuple[2] if isinstance(reason, str): return reason return None -def create_ssl_nonblocking_connection(mode: int) \ - -> tuple[socket, socket, Connection, Connection]: +def create_ssl_nonblocking_connection( + mode: int, +) -> tuple[socket, socket, Connection, Connection]: """ Create a pair of sockets and set up an SSL connection between them. """ @@ -462,8 +468,8 @@ def create_ssl_nonblocking_connection(mode: int) \ # these modes are set by default when ctx is initialized # clear them so we can run tests with or without them ctx.clear_mode( - _lib.SSL_MODE_ENABLE_PARTIAL_WRITE | - _lib.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER + _lib.SSL_MODE_ENABLE_PARTIAL_WRITE + | _lib.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER ) ctx.set_mode(mode) @@ -526,7 +532,7 @@ def create_ssl_nonblocking_connection(mode: int) \ [client_socket, server_socket], [client_socket, server_socket], [client_socket, server_socket], - 1.0 # 1 second timeout + 1.0, # 1 second timeout ) if ready_err: @@ -3131,8 +3137,9 @@ def _badwriteretry(self, mode: int) -> bool: by using a moving buffer. Returns True if a bad write retry error occurs. """ - client_socket, server_socket, client, server \ - = create_ssl_nonblocking_connection(mode) + client_socket, server_socket, client, server = ( + create_ssl_nonblocking_connection(mode) + ) result = False # Default return value written = 0 @@ -3221,8 +3228,10 @@ def test_moving_write_buffer_should_pass(self) -> None: result = self._badwriteretry(mode) if result: - pytest.fail("Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to \ - prevent bad write rety") + pytest.fail( + "Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to \ + prevent bad write rety" + ) def test_moving_write_buffer_should_fail(self) -> None: """ @@ -3236,8 +3245,10 @@ def test_moving_write_buffer_should_fail(self) -> None: result = self._badwriteretry(mode) if not result: - pytest.fail("Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to \ - prevent bad write rety") + pytest.fail( + "Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to \ + prevent bad write rety" + ) def test_get_finished_before_connect(self) -> None: """ From 46107db583b8c96840a1ff655fa50e60d4477a70 Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 20 Jul 2025 10:34:18 -0700 Subject: [PATCH 15/44] fix to clear_mode() decorator --- src/OpenSSL/SSL.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenSSL/SSL.py b/src/OpenSSL/SSL.py index 254d8720..0caa678f 100644 --- a/src/OpenSSL/SSL.py +++ b/src/OpenSSL/SSL.py @@ -1717,7 +1717,7 @@ def set_mode(self, mode: int) -> int: return _lib.SSL_CTX_set_mode(self._context, mode) - #@_require_not_used + @_require_not_used def clear_mode(self, mode_to_clear: int) -> int: """ Modes previously set cannot be overwritten without being From 08150c52ec2934422b1c34521580b0b2e9b1f694 Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 20 Jul 2025 10:49:12 -0700 Subject: [PATCH 16/44] Revert "removed Python 3.7 from the -cryptographyMain tests because it fails" This reverts commit 50654a8fd6fdb293c718060ad9b6ba0a40ecb3db. --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a29d2ad1..ebc0bf2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,7 @@ jobs: - {VERSION: "pypy-3.10", TOXENV: "pypy3"} - {VERSION: "3.11", TOXENV: "py311-useWheel", OS: "windows-2022" } # -cryptographyMain + - {VERSION: "3.7", TOXENV: "py37-cryptographyMain"} - {VERSION: "3.8", TOXENV: "py38-cryptographyMain"} - {VERSION: "3.9", TOXENV: "py39-cryptographyMain"} - {VERSION: "3.10", TOXENV: "py310-cryptographyMain"} @@ -31,7 +32,7 @@ jobs: - {VERSION: "pypy-3.9", TOXENV: "pypy3-cryptographyMain"} - {VERSION: "pypy-3.10", TOXENV: "pypy3-cryptographyMain"} # -cryptographyMinimum - + - {VERSION: "3.7", TOXENV: "py37-cryptographyMinimum"} - {VERSION: "3.8", TOXENV: "py38-cryptographyMinimum"} - {VERSION: "3.9", TOXENV: "py39-cryptographyMinimum"} - {VERSION: "3.10", TOXENV: "py310-cryptographyMinimum"} From 7d6365dcb41831e8887e7f69baeb1508d00605ab Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 20 Jul 2025 11:51:31 -0700 Subject: [PATCH 17/44] removed py37-cryptographyMain because of version conflicts --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebc0bf2c..11e5f9fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,6 @@ jobs: - {VERSION: "pypy-3.10", TOXENV: "pypy3"} - {VERSION: "3.11", TOXENV: "py311-useWheel", OS: "windows-2022" } # -cryptographyMain - - {VERSION: "3.7", TOXENV: "py37-cryptographyMain"} - {VERSION: "3.8", TOXENV: "py38-cryptographyMain"} - {VERSION: "3.9", TOXENV: "py39-cryptographyMain"} - {VERSION: "3.10", TOXENV: "py310-cryptographyMain"} From 8b2a84b5efde205002daa3e2735dbcd9d916529c Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 20 Jul 2025 20:53:08 -0700 Subject: [PATCH 18/44] fixed fail message in test_moving_write_buffer_should_fail --- tests/test_ssl.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 53b9f228..74b77ac1 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3246,8 +3246,9 @@ def test_moving_write_buffer_should_fail(self) -> None: if not result: pytest.fail( - "Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to \ - prevent bad write rety" + "Use of a moving buffer without \ + SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER should have \ + a bad write retry error" ) def test_get_finished_before_connect(self) -> None: From 3724e4e6e983571bb252b87a4a2c36e4c9e6a2fa Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 20 Jul 2025 20:58:43 -0700 Subject: [PATCH 19/44] fixed typo --- tests/test_ssl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 74b77ac1..2c84c2ce 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3230,7 +3230,7 @@ def test_moving_write_buffer_should_pass(self) -> None: if result: pytest.fail( "Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to \ - prevent bad write rety" + prevent bad write retry" ) def test_moving_write_buffer_should_fail(self) -> None: From d95dcf50b89a6781721f27824a077bbdcf25d151 Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 20 Jul 2025 21:02:16 -0700 Subject: [PATCH 20/44] fixed wording --- tests/test_ssl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 2c84c2ce..304312ea 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3247,7 +3247,7 @@ def test_moving_write_buffer_should_fail(self) -> None: if not result: pytest.fail( "Use of a moving buffer without \ - SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER should have \ + SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER should trigger \ a bad write retry error" ) From 891bdc4c2675f8fbfbe8d50e17e5089eab163c94 Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 21 Jul 2025 14:23:21 -0700 Subject: [PATCH 21/44] improved test for moving buffer --- tests/test_ssl.py | 207 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 191 insertions(+), 16 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 304312ea..3cf42317 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3132,6 +3132,8 @@ def test_wantWriteError(self) -> None: # XXX want_read def _badwriteretry(self, mode: int) -> bool: + import time + """ Tries to force a "bad write retry" error over an SSL connection by using a moving buffer. Returns True if a bad write retry @@ -3153,6 +3155,7 @@ def _badwriteretry(self, mode: int) -> bool: try: written = client_socket.send(msg) print(f"Sent {written} bytes to fill buffer") + time.sleep(0.01) except OSError as e: if e.errno == EWOULDBLOCK: break @@ -3165,21 +3168,185 @@ def _badwriteretry(self, mode: int) -> bool: # Now, attempt to send application data over the established # SSL connection. Since the underlying raw socket's buffer is full, - # this should cause a WantWriteError. - msg2 = b"Y" * 65536 + # this should cause a WantWriteError. Start with small messages + # and increase size until we get WantWriteError. This ensures we + # find the minimum size that triggers the error reliably. + test_sizes = [ + 64, + 128, + 256, + 512, + 1024, + 2048, + 4096, + 8192, + 16384, + 32768, + 65536, + ] + initial_want_write_triggered = False + successful_size = None + + for size in test_sizes: + msg2 = b"Y" * size + try: + written = client.send(msg2) + print( + f"Write succeeded with size {size}. " + f"wrote {written} bytes - trying larger size" + ) + successful_size = size + continue # Try next larger size + except SSL.WantWriteError: + print( + f"Got WantWriteError with message size {size} " + "(this is what we want)" + ) + initial_want_write_triggered = True + successful_size = size + break + + if not initial_want_write_triggered: + if successful_size: + print( + f"All sizes succeeded up to {successful_size}. " + "The buffer may not be full enough" + ) + pytest.fail( + "Could not trigger WantWriteError even with largest " + f"message size {test_sizes[-1]}" + ) + else: + pytest.fail("Could not send any message size") + + if initial_want_write_triggered: + # We got the WantWriteError we wanted, proceed with the test + # Start reading from the server connection to drain the buffer + # Since we filled the buffer with raw data, we need to read + # from the raw server socket, not the SSL connection. + total_read = 0 + read_chunks = [] + + try: + # First, try to read any SSL data that might be available + try: + ssl_data = server.recv(65536) + if ssl_data: + read_chunks.append(ssl_data) + total_read += len(ssl_data) + print( + f"Read {len(ssl_data)} bytes of " + "SSL data from server" + ) + time.sleep(0.01) # Small delay after SSL read + except (SSL.WantReadError, SSL.Error) as ssl_error: + print( + f"No SSL data available or SSL error: {ssl_error}" + ) + + # Now read raw data from the underlying server socket + # to drain buffer + server_socket.setblocking(False) # Ensure non-blocking + consecutive_empty_reads = 0 + + while ( + total_read < 1024 * 1024 + ): # Read up to 1MB or until no more data + try: + data = server_socket.recv( + 65536 + ) # Read raw data from underlying socket + if not data: + consecutive_empty_reads += 1 + if consecutive_empty_reads >= 3: + print( + "Multiple empty reads, assuming " + "no more data" + ) + break + time.sleep( + 0.05 + ) # Wait a bit for more data to arrive + continue + + consecutive_empty_reads = ( + 0 # Reset counter on successful read + ) + read_chunks.append(data) + total_read += len(data) + print( + f"Read {len(data)} bytes of raw data from " + f"server socket (total: {total_read})" + ) + + # Add small delay between reads to allow network + # buffers to settle + time.sleep(0.01) + + except OSError as e: + if e.errno == EWOULDBLOCK: + # No more data available right now, wait a bit + consecutive_empty_reads += 1 + if consecutive_empty_reads >= 5: + print( + "No more raw data available from " + "server socket after retries" + ) + break + print( + "No data available, waiting... " + f"(attempt {consecutive_empty_reads})" + ) + time.sleep( + 0.1 + ) # Wait longer when buffer is empty + continue + else: + print( + "OSError while reading from server" + f"socket: {e}" + ) + break + + print( + "Finished reading from server. " + f"Total bytes read: {total_read}" + ) + + # Give additional time for network stack to process + # the drained data + print("Allowing network buffers to settle...") + time.sleep(0.2) + + except Exception as read_exception: + print( + "Exception while reading from " + f"server: {read_exception}" + ) - try: - written = client.send(msg2) - except SSL.WantWriteError: try: # After a WantWriteError if the connection has partially # written the last buffer it will expect a retry write. - # This next write should fail but for two different reasons - # depending on whether SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER - # was set - msg3 = b"Z" * 65536 + # This next write should succeed only if + # SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER was set as we will + # use a DIFFERENT message from the first one to test moving + # buffer behavior. If SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER + # is NOT set, this should give "bad write retry" + msg3 = (b"Z" * successful_size) + # Same size, different content - + # this is the "moving buffer". + # By setting different content we guarantee a different + # buffer. If we set the same content Python will typically + # optimize to use ths same buffer as before + print( + "Attempting retry with different buffer " + f"(same size {successful_size})" + ) + written = client.send(msg3) - pytest.fail("Retry succeeded unexpectedly") + print(f"Retry succeeded with {written} bytes written") + result = False + # pytest.fail("Retry succeeded unexpectedly") except SSL.Error as e: reason = get_ssl_error_reason(e) if reason == "bad write retry": @@ -3189,13 +3356,21 @@ def _badwriteretry(self, mode: int) -> bool: else: # when using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER # we expect this to fail for a WantWriteError - result = False + pytest.fail("Retry failed with out bad write error") - except SSL.Error as e: - reason = get_ssl_error_reason(e) - pytest.fail(f"Got unexpected SSL error on retry: {e} {reason}") - except Exception as e: - pytest.fail(f"Unexpected exception during send: {e}") + else: + # This shouldn't happen given our logic above, + # but handle it gracefully + pytest.fail( + "Unexpected state: no WantWriteError triggered " + "and no successful size found" + ) + + except SSL.Error as e: + reason = get_ssl_error_reason(e) + pytest.fail(f"Got unexpected SSL error: {e} {reason}") + except Exception as e: + pytest.fail(f"Unexpected exception during send: {e}") finally: # Cleanup: shut down SSL connections and close raw sockets From 9fa129036da6f93ca75b8411d48475d92d9f5b73 Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 21 Jul 2025 14:32:50 -0700 Subject: [PATCH 22/44] format fix --- tests/test_ssl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 3cf42317..cc9b0138 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3332,7 +3332,7 @@ def _badwriteretry(self, mode: int) -> bool: # use a DIFFERENT message from the first one to test moving # buffer behavior. If SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER # is NOT set, this should give "bad write retry" - msg3 = (b"Z" * successful_size) + msg3 = b"Z" * successful_size # Same size, different content - # this is the "moving buffer". # By setting different content we guarantee a different From fffb607021f403bcb5505659345aef5831367429 Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 21 Jul 2025 15:20:05 -0700 Subject: [PATCH 23/44] type correction for mypy --- tests/test_ssl.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index cc9b0138..f630f183 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3332,6 +3332,9 @@ def _badwriteretry(self, mode: int) -> bool: # use a DIFFERENT message from the first one to test moving # buffer behavior. If SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER # is NOT set, this should give "bad write retry" + assert successful_size is not None, ( + "successful_size must be an int here as WantWriteError was triggered" + ) msg3 = b"Z" * successful_size # Same size, different content - # this is the "moving buffer". From 2c638f7490d1544711038c1403a6b63b9a12be36 Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 21 Jul 2025 15:26:30 -0700 Subject: [PATCH 24/44] fix for lint --- tests/test_ssl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index f630f183..51a9ac45 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3333,7 +3333,8 @@ def _badwriteretry(self, mode: int) -> bool: # buffer behavior. If SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER # is NOT set, this should give "bad write retry" assert successful_size is not None, ( - "successful_size must be an int here as WantWriteError was triggered" + "successful_size must be an int here as WantWriteError" + " was triggered" ) msg3 = b"Z" * successful_size # Same size, different content - From 234818e90ece750c4b10352b61fa27592fddb0f2 Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 21 Jul 2025 16:55:31 -0700 Subject: [PATCH 25/44] refactored moving buffer tests --- tests/test_ssl.py | 463 +++++++++++++++++++++++----------------------- 1 file changed, 233 insertions(+), 230 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 51a9ac45..f384f621 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3131,267 +3131,270 @@ def test_wantWriteError(self) -> None: # XXX want_read - def _badwriteretry(self, mode: int) -> bool: - import time - + def _fill_client_buffer(self, client_socket) -> None: """ - Tries to force a "bad write retry" error over an SSL connection - by using a moving buffer. Returns True if a bad write retry - error occurs. + Attempts to fill the client's raw send buffer until + EWOULDBLOCK is hit. """ - client_socket, server_socket, client, server = ( - create_ssl_nonblocking_connection(mode) - ) - result = False # Default return value - written = 0 + print("--- Phase 1: Filling client socket buffer ---") + for msg in [b"x" * 65536, b"x" * 16]: + for _ in range( + 1024 * 1024 * 64 + ): # Large loop count to ensure buffer fill + try: + client_socket.send(msg) + # print(f"Sent {written} bytes to fill buffer") + time.sleep(0.01) + except OSError as e: + if e.errno == EWOULDBLOCK: + print("Client socket buffer filled (EWOULDBLOCK hit).") + return # Buffer successfully filled, exit function + raise # Re-raise other unexpected OSErrors + else: # If the inner loop completes without hitting EWOULDBLOCK + pytest.fail( + "Failed to fill socket buffer, cannot test bad write error" + ) - try: - # Fill up the client's raw send buffer so the SSL connection - # won't be able to write anything. Start by sending larger chunks - # and continue by writing smaller chunks so we can be sure we - # completely fill the buffer. - for msg in [b"x" * 65536, b"x" * 16]: - for i in range(1024 * 1024 * 64): - try: - written = client_socket.send(msg) - print(f"Sent {written} bytes to fill buffer") - time.sleep(0.01) - except OSError as e: - if e.errno == EWOULDBLOCK: - break - raise - else: - pytest.fail( - "Failed to fill socket buffer, cannot test \ - bad write error" - ) + def _attempt_want_write_error( + self, client + ) -> typing.Tuple[bool, typing.Optional[int]]: + """ + Attempts to send application data over SSL to trigger WantWriteError. + Returns (True, successful_size) if triggered, + otherwise calls pytest.fail. + """ + print("--- Phase 2: Attempting to trigger WantWriteError ---") + test_sizes = [ + 64, + 128, + 256, + 512, + 1024, + 2048, + 4096, + 8192, + 16384, + 32768, + 65536, + ] + initial_want_write_triggered = False + successful_size: typing.Optional[int] = None - # Now, attempt to send application data over the established - # SSL connection. Since the underlying raw socket's buffer is full, - # this should cause a WantWriteError. Start with small messages - # and increase size until we get WantWriteError. This ensures we - # find the minimum size that triggers the error reliably. - test_sizes = [ - 64, - 128, - 256, - 512, - 1024, - 2048, - 4096, - 8192, - 16384, - 32768, - 65536, - ] - initial_want_write_triggered = False - successful_size = None + for size in test_sizes: + msg2 = b"Y" * size + try: + client.send(msg2) + print( + f"Write succeeded with size {size}, trying larger size..." + ) + successful_size = size + # Continue loop to try larger sizes until an error is hit + except SSL.WantWriteError: + print( + f"Got WantWriteError with message size {size} " + "(this is what we want)." + ) + initial_want_write_triggered = True + successful_size = size * 2 # double it to be really sure + break # Exit loop as desired error was triggered + + # Logic from the original 'if not initial_want_write_triggered' block + if not initial_want_write_triggered: + if successful_size is not None: + print( + f"All sizes succeeded up to {successful_size}. " + "The buffer may not be full enough." + ) + pytest.fail( + "Could not trigger WantWriteError even with largest " + f"message size {test_sizes[-1]}" + ) + else: + pytest.fail("Could not send any message size.") - for size in test_sizes: - msg2 = b"Y" * size - try: - written = client.send(msg2) - print( - f"Write succeeded with size {size}. " - f"wrote {written} bytes - trying larger size" - ) - successful_size = size - continue # Try next larger size - except SSL.WantWriteError: + return initial_want_write_triggered, successful_size + + def _drain_server_buffers(self, server, server_socket) -> None: + """Reads from server SSL and raw sockets to drain any pending data.""" + print("--- Phase 3: Draining server buffers ---") + total_read = 0 + read_chunks = [] + + try: + # First, try to read any SSL data that might be available + try: + ssl_data = server.recv(65536) + if ssl_data: + read_chunks.append(ssl_data) + total_read += len(ssl_data) print( - f"Got WantWriteError with message size {size} " - "(this is what we want)" + f"Read {len(ssl_data)} bytes of SSL data from server." ) - initial_want_write_triggered = True - successful_size = size - break + time.sleep(0.01) # Small delay after SSL read + except (SSL.WantReadError, SSL.Error) as ssl_error: + print(f"No SSL data available or SSL error: {ssl_error}") + + # Now read raw data from the underlying server socket to + # drain buffer + server_socket.setblocking(False) # Ensure non-blocking + consecutive_empty_reads = 0 + + while ( + total_read < 1024 * 1024 + ): # Read up to 1MB or until no more data + try: + data = server_socket.recv(65536) # Read raw data + if not data: + consecutive_empty_reads += 1 + if consecutive_empty_reads >= 3: + print( + "Multiple empty reads, assuming no more data." + ) + break + time.sleep(0.05) # Wait a bit for more data to arrive + continue - if not initial_want_write_triggered: - if successful_size: + consecutive_empty_reads = 0 # Reset counter + read_chunks.append(data) + total_read += len(data) print( - f"All sizes succeeded up to {successful_size}. " - "The buffer may not be full enough" + f"Read {len(data)} bytes of raw data from " + f"server socket (total: {total_read})." ) - pytest.fail( - "Could not trigger WantWriteError even with largest " - f"message size {test_sizes[-1]}" - ) - else: - pytest.fail("Could not send any message size") - - if initial_want_write_triggered: - # We got the WantWriteError we wanted, proceed with the test - # Start reading from the server connection to drain the buffer - # Since we filled the buffer with raw data, we need to read - # from the raw server socket, not the SSL connection. - total_read = 0 - read_chunks = [] + time.sleep(0.01) # Small delay between reads - try: - # First, try to read any SSL data that might be available - try: - ssl_data = server.recv(65536) - if ssl_data: - read_chunks.append(ssl_data) - total_read += len(ssl_data) + except OSError as e: + if e.errno == EWOULDBLOCK: + consecutive_empty_reads += 1 + if consecutive_empty_reads >= 5: print( - f"Read {len(ssl_data)} bytes of " - "SSL data from server" + "No more raw data available from server " + "socket after retries." ) - time.sleep(0.01) # Small delay after SSL read - except (SSL.WantReadError, SSL.Error) as ssl_error: + break print( - f"No SSL data available or SSL error: {ssl_error}" + "No data available, waiting... " + f"(attempt {consecutive_empty_reads})." ) + time.sleep(0.1) # Wait longer when buffer is empty + continue + else: + print(f"OSError while reading from server socket: {e}") + break - # Now read raw data from the underlying server socket - # to drain buffer - server_socket.setblocking(False) # Ensure non-blocking - consecutive_empty_reads = 0 - - while ( - total_read < 1024 * 1024 - ): # Read up to 1MB or until no more data - try: - data = server_socket.recv( - 65536 - ) # Read raw data from underlying socket - if not data: - consecutive_empty_reads += 1 - if consecutive_empty_reads >= 3: - print( - "Multiple empty reads, assuming " - "no more data" - ) - break - time.sleep( - 0.05 - ) # Wait a bit for more data to arrive - continue - - consecutive_empty_reads = ( - 0 # Reset counter on successful read - ) - read_chunks.append(data) - total_read += len(data) - print( - f"Read {len(data)} bytes of raw data from " - f"server socket (total: {total_read})" - ) + print( + f"Finished reading from server. Total bytes read: {total_read}" + ) + print("Allowing network buffers to settle...") + time.sleep(0.1) - # Add small delay between reads to allow network - # buffers to settle - time.sleep(0.01) - - except OSError as e: - if e.errno == EWOULDBLOCK: - # No more data available right now, wait a bit - consecutive_empty_reads += 1 - if consecutive_empty_reads >= 5: - print( - "No more raw data available from " - "server socket after retries" - ) - break - print( - "No data available, waiting... " - f"(attempt {consecutive_empty_reads})" - ) - time.sleep( - 0.1 - ) # Wait longer when buffer is empty - continue - else: - print( - "OSError while reading from server" - f"socket: {e}" - ) - break + except Exception as read_exception: + print(f"Exception while reading from server: {read_exception}") - print( - "Finished reading from server. " - f"Total bytes read: {total_read}" - ) + def _perform_moving_buffer_test( + self, client, successful_size: int + ) -> bool: + """ + Attempts a retry write with a moving buffer and checks for + 'bad write retry' error. Assumes successful_size is an int. + Returns True if 'bad write retry' occurs, False otherwise. + """ + print("--- Phase 4: Performing moving buffer retry test ---") + # Assert added for MyPy as discussed previously + assert successful_size is not None, ( + "successful_size must be an int here as WantWriteError " + "was triggered" + ) + msg3 = b"Z" * successful_size - # Give additional time for network stack to process - # the drained data - print("Allowing network buffers to settle...") - time.sleep(0.2) + print( + "Attempting retry with different buffer " + f"(same size {successful_size})." + ) - except Exception as read_exception: - print( - "Exception while reading from " - f"server: {read_exception}" - ) + try: + client.send(msg3) + print(f"Retry succeeded with {successful_size} bytes written.") + return False # Retry succeeded unexpectedly + except SSL.Error as e: + reason = get_ssl_error_reason(e) + if reason == "bad write retry": + print(f"Got expected SSL error: {e} ({reason}).") + return True # Expected error + else: + pytest.fail( + f"Retry failed with unexpected SSL error: {e} ({reason})." + ) + # If any other exception occurs, it will propagate up - try: - # After a WantWriteError if the connection has partially - # written the last buffer it will expect a retry write. - # This next write should succeed only if - # SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER was set as we will - # use a DIFFERENT message from the first one to test moving - # buffer behavior. If SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER - # is NOT set, this should give "bad write retry" - assert successful_size is not None, ( - "successful_size must be an int here as WantWriteError" - " was triggered" - ) - msg3 = b"Z" * successful_size - # Same size, different content - - # this is the "moving buffer". - # By setting different content we guarantee a different - # buffer. If we set the same content Python will typically - # optimize to use ths same buffer as before - print( - "Attempting retry with different buffer " - f"(same size {successful_size})" - ) + def _shutdown_connections( + self, client, server, client_socket, server_socket + ) -> None: + """Helper to safely shut down SSL connections and close sockets.""" + print("--- Cleanup: Shutting down connections ---") + try: + if client: + client.shutdown() + except Exception as e: + print(f"Error during client SSL shutdown: {e}") + try: + if server: + server.shutdown() + except Exception as e: + print(f"Error during server SSL shutdown: {e}") + finally: + if client_socket: + client_socket.close() + if server_socket: + server_socket.close() + print("Connections closed.") - written = client.send(msg3) - print(f"Retry succeeded with {written} bytes written") - result = False - # pytest.fail("Retry succeeded unexpectedly") - except SSL.Error as e: - reason = get_ssl_error_reason(e) - if reason == "bad write retry": - # Got SSL error on retry (expected if not using \ - # SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER) - result = True - else: - # when using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER - # we expect this to fail for a WantWriteError - pytest.fail("Retry failed with out bad write error") + def _badwriteretry(self, mode: int) -> bool: + """ + Tries to force a "bad write retry" error over an SSL connection + by using a moving buffer. Returns True if a bad write retry + error occurs. + """ + client_socket, server_socket, client, server = ( + create_ssl_nonblocking_connection(mode) + ) + result = False # Default return value + + try: + # --- Main Test Flow --- + self._fill_client_buffer(client_socket) + + initial_want_write_triggered, successful_size = ( + self._attempt_want_write_error(client) + ) + if initial_want_write_triggered: + # If WantWriteError was successfully triggered, proceed with + # draining and retry + self._drain_server_buffers(server, server_socket) + result = self._perform_moving_buffer_test( + client, successful_size + ) else: - # This shouldn't happen given our logic above, - # but handle it gracefully + # This branch should ideally be unreachable because + # _attempt_want_write_error already calls pytest.fail if + # initial_want_write_triggered is False. Keeping it here for + # explicit flow, but it implies a logic error if reached. pytest.fail( - "Unexpected state: no WantWriteError triggered " - "and no successful size found" + "Unexpected state: WantWriteError was not triggered." ) except SSL.Error as e: reason = get_ssl_error_reason(e) - pytest.fail(f"Got unexpected SSL error: {e} {reason}") + pytest.fail(f"Got unexpected SSL error: {e} ({reason}).") except Exception as e: - pytest.fail(f"Unexpected exception during send: {e}") - + pytest.fail(f"Unexpected exception during test: {e}.") finally: - # Cleanup: shut down SSL connections and close raw sockets - try: - if client: - client.shutdown() - if server: - server.shutdown() - except Exception as e: - print(f"Error during SSL shutdown: {e}") - finally: - if client_socket: - client_socket.close() - if server_socket: - server_socket.close() - - return result # Return the result after cleanup + self._shutdown_connections( + client, server, client_socket, server_socket + ) + + return result def test_moving_write_buffer_should_pass(self) -> None: """ From 666fb00b3eecd8b6a0c381a0565879d4c2d73f91 Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 21 Jul 2025 17:47:34 -0700 Subject: [PATCH 26/44] type fixes for mypy --- tests/test_ssl.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index f384f621..b48d5f57 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3131,7 +3131,7 @@ def test_wantWriteError(self) -> None: # XXX want_read - def _fill_client_buffer(self, client_socket) -> None: + def _fill_client_buffer(self, client_socket: socket) -> None: """ Attempts to fill the client's raw send buffer until EWOULDBLOCK is hit. @@ -3156,8 +3156,8 @@ def _fill_client_buffer(self, client_socket) -> None: ) def _attempt_want_write_error( - self, client - ) -> typing.Tuple[bool, typing.Optional[int]]: + self, client: Connection + ) -> tuple[bool, int]: """ Attempts to send application data over SSL to trigger WantWriteError. Returns (True, successful_size) if triggered, @@ -3178,7 +3178,7 @@ def _attempt_want_write_error( 65536, ] initial_want_write_triggered = False - successful_size: typing.Optional[int] = None + successful_size = -1 for size in test_sizes: msg2 = b"Y" * size @@ -3200,7 +3200,7 @@ def _attempt_want_write_error( # Logic from the original 'if not initial_want_write_triggered' block if not initial_want_write_triggered: - if successful_size is not None: + if successful_size > -1: print( f"All sizes succeeded up to {successful_size}. " "The buffer may not be full enough." @@ -3214,7 +3214,9 @@ def _attempt_want_write_error( return initial_want_write_triggered, successful_size - def _drain_server_buffers(self, server, server_socket) -> None: + def _drain_server_buffers( + self, server: Connection, + server_socket: socket) -> None: """Reads from server SSL and raw sockets to drain any pending data.""" print("--- Phase 3: Draining server buffers ---") total_read = 0 @@ -3292,7 +3294,7 @@ def _drain_server_buffers(self, server, server_socket) -> None: print(f"Exception while reading from server: {read_exception}") def _perform_moving_buffer_test( - self, client, successful_size: int + self, client: Connection, successful_size: int ) -> bool: """ Attempts a retry write with a moving buffer and checks for @@ -3301,9 +3303,9 @@ def _perform_moving_buffer_test( """ print("--- Phase 4: Performing moving buffer retry test ---") # Assert added for MyPy as discussed previously - assert successful_size is not None, ( - "successful_size must be an int here as WantWriteError " - "was triggered" + assert successful_size > -1, ( + "successful_size must be greater than -1 for a WantWriteError " + "to be triggered" ) msg3 = b"Z" * successful_size @@ -3328,7 +3330,11 @@ def _perform_moving_buffer_test( # If any other exception occurs, it will propagate up def _shutdown_connections( - self, client, server, client_socket, server_socket + self, + client: Connection, + server: Connection, + client_socket: socket, + server_socket: socket ) -> None: """Helper to safely shut down SSL connections and close sockets.""" print("--- Cleanup: Shutting down connections ---") From 704767f74717fcd4a4c1bba19214561205482cd5 Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 21 Jul 2025 17:53:33 -0700 Subject: [PATCH 27/44] fix for ruff --- tests/test_ssl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index b48d5f57..75db047f 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3215,8 +3215,8 @@ def _attempt_want_write_error( return initial_want_write_triggered, successful_size def _drain_server_buffers( - self, server: Connection, - server_socket: socket) -> None: + self, server: Connection, server_socket: socket + ) -> None: """Reads from server SSL and raw sockets to drain any pending data.""" print("--- Phase 3: Draining server buffers ---") total_read = 0 @@ -3334,7 +3334,7 @@ def _shutdown_connections( client: Connection, server: Connection, client_socket: socket, - server_socket: socket + server_socket: socket, ) -> None: """Helper to safely shut down SSL connections and close sockets.""" print("--- Cleanup: Shutting down connections ---") From 87327ecf3c87edc6aaaa618d60d1778d39fc5f22 Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 21 Jul 2025 18:25:55 -0700 Subject: [PATCH 28/44] fixed test_wantWriteError to work better on MacOS --- tests/test_ssl.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 75db047f..4d364874 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3109,12 +3109,13 @@ def test_wantWriteError(self) -> None: # signal a short write via its return value it seems this doesn't # always happen on all platforms (FreeBSD and OS X particular) for the # very last bit of available buffer space. - for msg in [b"x" * 65536, b"x"]: + for msg in [b"x" * 65536, b"x" * 16, b"x"]: for i in range(1024 * 1024 * 64): try: client_socket.send(msg) except OSError as e: if e.errno == EWOULDBLOCK: + time.sleep(0.1) break raise # pragma: no cover else: # pragma: no cover @@ -3298,11 +3299,10 @@ def _perform_moving_buffer_test( ) -> bool: """ Attempts a retry write with a moving buffer and checks for - 'bad write retry' error. Assumes successful_size is an int. + 'bad write retry' error. Returns True if 'bad write retry' occurs, False otherwise. """ print("--- Phase 4: Performing moving buffer retry test ---") - # Assert added for MyPy as discussed previously assert successful_size > -1, ( "successful_size must be greater than -1 for a WantWriteError " "to be triggered" @@ -3317,12 +3317,12 @@ def _perform_moving_buffer_test( try: client.send(msg3) print(f"Retry succeeded with {successful_size} bytes written.") - return False # Retry succeeded unexpectedly + return False # Retry succeeded except SSL.Error as e: reason = get_ssl_error_reason(e) if reason == "bad write retry": print(f"Got expected SSL error: {e} ({reason}).") - return True # Expected error + return True # Bad write retry else: pytest.fail( f"Retry failed with unexpected SSL error: {e} ({reason})." @@ -3389,10 +3389,6 @@ def _badwriteretry(self, mode: int) -> bool: pytest.fail( "Unexpected state: WantWriteError was not triggered." ) - - except SSL.Error as e: - reason = get_ssl_error_reason(e) - pytest.fail(f"Got unexpected SSL error: {e} ({reason}).") except Exception as e: pytest.fail(f"Unexpected exception during test: {e}.") finally: From a402470f429b1dd658575d0d9e792e93b329317e Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 21 Jul 2025 20:26:38 -0700 Subject: [PATCH 29/44] improved code coverage --- tests/test_ssl.py | 102 ++++++++++++---------------------------------- 1 file changed, 27 insertions(+), 75 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 4d364874..a096b245 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3144,21 +3144,18 @@ def _fill_client_buffer(self, client_socket: socket) -> None: ): # Large loop count to ensure buffer fill try: client_socket.send(msg) - # print(f"Sent {written} bytes to fill buffer") time.sleep(0.01) except OSError as e: if e.errno == EWOULDBLOCK: print("Client socket buffer filled (EWOULDBLOCK hit).") return # Buffer successfully filled, exit function - raise # Re-raise other unexpected OSErrors + raise # pragma: no cover # Re-raise unexpected OSErrors else: # If the inner loop completes without hitting EWOULDBLOCK pytest.fail( "Failed to fill socket buffer, cannot test bad write error" - ) + ) # pragma: no cover - def _attempt_want_write_error( - self, client: Connection - ) -> tuple[bool, int]: + def _attempt_want_write_error(self, client: Connection) -> int: """ Attempts to send application data over SSL to trigger WantWriteError. Returns (True, successful_size) if triggered, @@ -3185,10 +3182,6 @@ def _attempt_want_write_error( msg2 = b"Y" * size try: client.send(msg2) - print( - f"Write succeeded with size {size}, trying larger size..." - ) - successful_size = size # Continue loop to try larger sizes until an error is hit except SSL.WantWriteError: print( @@ -3199,21 +3192,12 @@ def _attempt_want_write_error( successful_size = size * 2 # double it to be really sure break # Exit loop as desired error was triggered - # Logic from the original 'if not initial_want_write_triggered' block if not initial_want_write_triggered: - if successful_size > -1: - print( - f"All sizes succeeded up to {successful_size}. " - "The buffer may not be full enough." - ) - pytest.fail( - "Could not trigger WantWriteError even with largest " - f"message size {test_sizes[-1]}" - ) - else: - pytest.fail("Could not send any message size.") + pytest.fail( + "Could not induce WantWriteError with any message size." + ) # pragma: no cover - return initial_want_write_triggered, successful_size + return successful_size def _drain_server_buffers( self, server: Connection, server_socket: socket @@ -3226,14 +3210,7 @@ def _drain_server_buffers( try: # First, try to read any SSL data that might be available try: - ssl_data = server.recv(65536) - if ssl_data: - read_chunks.append(ssl_data) - total_read += len(ssl_data) - print( - f"Read {len(ssl_data)} bytes of SSL data from server." - ) - time.sleep(0.01) # Small delay after SSL read + server.recv(65536) except (SSL.WantReadError, SSL.Error) as ssl_error: print(f"No SSL data available or SSL error: {ssl_error}") @@ -3247,17 +3224,6 @@ def _drain_server_buffers( ): # Read up to 1MB or until no more data try: data = server_socket.recv(65536) # Read raw data - if not data: - consecutive_empty_reads += 1 - if consecutive_empty_reads >= 3: - print( - "Multiple empty reads, assuming no more data." - ) - break - time.sleep(0.05) # Wait a bit for more data to arrive - continue - - consecutive_empty_reads = 0 # Reset counter read_chunks.append(data) total_read += len(data) print( @@ -3281,7 +3247,7 @@ def _drain_server_buffers( ) time.sleep(0.1) # Wait longer when buffer is empty continue - else: + else: # pragma: no cover print(f"OSError while reading from server socket: {e}") break @@ -3291,7 +3257,7 @@ def _drain_server_buffers( print("Allowing network buffers to settle...") time.sleep(0.1) - except Exception as read_exception: + except Exception as read_exception: # pragma: no cover print(f"Exception while reading from server: {read_exception}") def _perform_moving_buffer_test( @@ -3326,7 +3292,7 @@ def _perform_moving_buffer_test( else: pytest.fail( f"Retry failed with unexpected SSL error: {e} ({reason})." - ) + ) # pragma: no cover # If any other exception occurs, it will propagate up def _shutdown_connections( @@ -3370,26 +3336,14 @@ def _badwriteretry(self, mode: int) -> bool: # --- Main Test Flow --- self._fill_client_buffer(client_socket) - initial_want_write_triggered, successful_size = ( - self._attempt_want_write_error(client) - ) + successful_size = self._attempt_want_write_error(client) - if initial_want_write_triggered: - # If WantWriteError was successfully triggered, proceed with - # draining and retry - self._drain_server_buffers(server, server_socket) - result = self._perform_moving_buffer_test( - client, successful_size - ) - else: - # This branch should ideally be unreachable because - # _attempt_want_write_error already calls pytest.fail if - # initial_want_write_triggered is False. Keeping it here for - # explicit flow, but it implies a logic error if reached. - pytest.fail( - "Unexpected state: WantWriteError was not triggered." - ) - except Exception as e: + # if WantWriteError was not triggered the test fails in + # _attempt_want_write_error(). + # proceed with draining and retry + self._drain_server_buffers(server, server_socket) + result = self._perform_moving_buffer_test(client, successful_size) + except Exception as e: # pragma: no cover pytest.fail(f"Unexpected exception during test: {e}.") finally: self._shutdown_connections( @@ -3411,11 +3365,10 @@ def test_moving_write_buffer_should_pass(self) -> None: ) result = self._badwriteretry(mode) - if result: - pytest.fail( - "Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to \ - prevent bad write retry" - ) + assert result is False, ( + "Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to prevent bad " + "write retry. A bad write retry occurred when it should not have." + ) def test_moving_write_buffer_should_fail(self) -> None: """ @@ -3428,12 +3381,11 @@ def test_moving_write_buffer_should_fail(self) -> None: mode = 0 result = self._badwriteretry(mode) - if not result: - pytest.fail( - "Use of a moving buffer without \ - SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER should trigger \ - a bad write retry error" - ) + assert result is True, ( + "Use of a moving buffer without " + "SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER should trigger " + "a bad write retry error." + ) def test_get_finished_before_connect(self) -> None: """ From 69de9ea1e1f5f23579ce3ea8f77f71e7929fcf6f Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 21 Jul 2025 14:23:21 -0700 Subject: [PATCH 30/44] improved test for moving buffer --- tests/test_ssl.py | 295 +++++++++++++++++++++++++++++++++------------- 1 file changed, 215 insertions(+), 80 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 304312ea..e0d1b999 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3131,6 +3131,195 @@ def test_wantWriteError(self) -> None: # XXX want_read + def _fill_client_buffer(self, client_socket: socket) -> None: + """ + Attempts to fill the client's raw send buffer until + EWOULDBLOCK is hit. + """ + print("--- Phase 1: Filling client socket buffer ---") + for msg in [b"x" * 65536, b"x" * 16]: + for _ in range( + 1024 * 1024 * 64 + ): # Large loop count to ensure buffer fill + try: + client_socket.send(msg) + time.sleep(0.01) + except OSError as e: + if e.errno == EWOULDBLOCK: + print("Client socket buffer filled (EWOULDBLOCK hit).") + return # Buffer successfully filled, exit function + raise # pragma: no cover # Re-raise unexpected OSErrors + else: # If the inner loop completes without hitting EWOULDBLOCK + pytest.fail( + "Failed to fill socket buffer, cannot test bad write error" + ) # pragma: no cover + + def _attempt_want_write_error(self, client: Connection) -> int: + """ + Attempts to send application data over SSL to trigger WantWriteError. + Returns (True, successful_size) if triggered, + otherwise calls pytest.fail. + """ + print("--- Phase 2: Attempting to trigger WantWriteError ---") + test_sizes = [ + 64, + 128, + 256, + 512, + 1024, + 2048, + 4096, + 8192, + 16384, + 32768, + 65536, + ] + initial_want_write_triggered = False + successful_size = -1 + + for size in test_sizes: + msg2 = b"Y" * size + try: + client.send(msg2) + # Continue loop to try larger sizes until an error is hit + except SSL.WantWriteError: + print( + f"Got WantWriteError with message size {size} " + "(this is what we want)." + ) + initial_want_write_triggered = True + successful_size = size * 2 # double it to be really sure + break # Exit loop as desired error was triggered + + if not initial_want_write_triggered: + pytest.fail( + "Could not induce WantWriteError with any message size." + ) # pragma: no cover + + return successful_size + + def _drain_server_buffers( + self, server: Connection, server_socket: socket + ) -> None: + """Reads from server SSL and raw sockets to drain any pending data.""" + print("--- Phase 3: Draining server buffers ---") + total_read = 0 + read_chunks = [] + + try: + # First, try to read any SSL data that might be available + try: + server.recv(65536) + except (SSL.WantReadError, SSL.Error) as ssl_error: + print(f"No SSL data available or SSL error: {ssl_error}") + + # Now read raw data from the underlying server socket to + # drain buffer + server_socket.setblocking(False) # Ensure non-blocking + consecutive_empty_reads = 0 + + while ( + total_read < 1024 * 1024 + ): # Read up to 1MB or until no more data + try: + data = server_socket.recv(65536) # Read raw data + read_chunks.append(data) + total_read += len(data) + print( + f"Read {len(data)} bytes of raw data from " + f"server socket (total: {total_read})." + ) + time.sleep(0.01) # Small delay between reads + + except OSError as e: + if e.errno == EWOULDBLOCK: + consecutive_empty_reads += 1 + if consecutive_empty_reads >= 5: + print( + "No more raw data available from server " + "socket after retries." + ) + break + print( + "No data available, waiting... " + f"(attempt {consecutive_empty_reads})." + ) + time.sleep(0.1) # Wait longer when buffer is empty + continue + else: # pragma: no cover + print(f"OSError while reading from server socket: {e}") + break + + print( + f"Finished reading from server. Total bytes read: {total_read}" + ) + print("Allowing network buffers to settle...") + time.sleep(0.1) + + except Exception as read_exception: # pragma: no cover + print(f"Exception while reading from server: {read_exception}") + + def _perform_moving_buffer_test( + self, client: Connection, successful_size: int + ) -> bool: + """ + Attempts a retry write with a moving buffer and checks for + 'bad write retry' error. + Returns True if 'bad write retry' occurs, False otherwise. + """ + print("--- Phase 4: Performing moving buffer retry test ---") + assert successful_size > -1, ( + "successful_size must be greater than -1 for a WantWriteError " + "to be triggered" + ) + msg3 = b"Z" * successful_size + + print( + "Attempting retry with different buffer " + f"(same size {successful_size})." + ) + + try: + client.send(msg3) + print(f"Retry succeeded with {successful_size} bytes written.") + return False # Retry succeeded + except SSL.Error as e: + reason = get_ssl_error_reason(e) + if reason == "bad write retry": + print(f"Got expected SSL error: {e} ({reason}).") + return True # Bad write retry + else: + pytest.fail( + f"Retry failed with unexpected SSL error: {e} ({reason})." + ) # pragma: no cover + # If any other exception occurs, it will propagate up + + def _shutdown_connections( + self, + client: Connection, + server: Connection, + client_socket: socket, + server_socket: socket, + ) -> None: + """Helper to safely shut down SSL connections and close sockets.""" + print("--- Cleanup: Shutting down connections ---") + try: + if client: + client.shutdown() + except Exception as e: + print(f"Error during client SSL shutdown: {e}") + try: + if server: + server.shutdown() + except Exception as e: + print(f"Error during server SSL shutdown: {e}") + finally: + if client_socket: + client_socket.close() + if server_socket: + server_socket.close() + print("Connections closed.") + def _badwriteretry(self, mode: int) -> bool: """ Tries to force a "bad write retry" error over an SSL connection @@ -3141,78 +3330,26 @@ def _badwriteretry(self, mode: int) -> bool: create_ssl_nonblocking_connection(mode) ) result = False # Default return value - written = 0 try: - # Fill up the client's raw send buffer so the SSL connection - # won't be able to write anything. Start by sending larger chunks - # and continue by writing smaller chunks so we can be sure we - # completely fill the buffer. - for msg in [b"x" * 65536, b"x" * 16]: - for i in range(1024 * 1024 * 64): - try: - written = client_socket.send(msg) - print(f"Sent {written} bytes to fill buffer") - except OSError as e: - if e.errno == EWOULDBLOCK: - break - raise - else: - pytest.fail( - "Failed to fill socket buffer, cannot test \ - bad write error" - ) - - # Now, attempt to send application data over the established - # SSL connection. Since the underlying raw socket's buffer is full, - # this should cause a WantWriteError. - msg2 = b"Y" * 65536 - - try: - written = client.send(msg2) - except SSL.WantWriteError: - try: - # After a WantWriteError if the connection has partially - # written the last buffer it will expect a retry write. - # This next write should fail but for two different reasons - # depending on whether SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER - # was set - msg3 = b"Z" * 65536 - written = client.send(msg3) - pytest.fail("Retry succeeded unexpectedly") - except SSL.Error as e: - reason = get_ssl_error_reason(e) - if reason == "bad write retry": - # Got SSL error on retry (expected if not using \ - # SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER) - result = True - else: - # when using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER - # we expect this to fail for a WantWriteError - result = False - - except SSL.Error as e: - reason = get_ssl_error_reason(e) - pytest.fail(f"Got unexpected SSL error on retry: {e} {reason}") - except Exception as e: - pytest.fail(f"Unexpected exception during send: {e}") - + # --- Main Test Flow --- + self._fill_client_buffer(client_socket) + + successful_size = self._attempt_want_write_error(client) + + # if WantWriteError was not triggered the test fails in + # _attempt_want_write_error(). + # proceed with draining and retry + self._drain_server_buffers(server, server_socket) + result = self._perform_moving_buffer_test(client, successful_size) + except Exception as e: # pragma: no cover + pytest.fail(f"Unexpected exception during test: {e}.") finally: - # Cleanup: shut down SSL connections and close raw sockets - try: - if client: - client.shutdown() - if server: - server.shutdown() - except Exception as e: - print(f"Error during SSL shutdown: {e}") - finally: - if client_socket: - client_socket.close() - if server_socket: - server_socket.close() - - return result # Return the result after cleanup + self._shutdown_connections( + client, server, client_socket, server_socket + ) + + return result def test_moving_write_buffer_should_pass(self) -> None: """ @@ -3227,11 +3364,10 @@ def test_moving_write_buffer_should_pass(self) -> None: ) result = self._badwriteretry(mode) - if result: - pytest.fail( - "Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to \ - prevent bad write retry" - ) + assert result is False, ( + "Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to prevent bad " + "write retry. A bad write retry occurred when it should not have." + ) def test_moving_write_buffer_should_fail(self) -> None: """ @@ -3244,12 +3380,11 @@ def test_moving_write_buffer_should_fail(self) -> None: mode = 0 result = self._badwriteretry(mode) - if not result: - pytest.fail( - "Use of a moving buffer without \ - SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER should trigger \ - a bad write retry error" - ) + assert result is True, ( + "Use of a moving buffer without " + "SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER should trigger " + "a bad write retry error." + ) def test_get_finished_before_connect(self) -> None: """ From 93bf6b301b56189aa20ad75d66fcca1a732a147c Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 21 Jul 2025 23:15:39 -0700 Subject: [PATCH 31/44] fixed wording on _attempt_want_write_error --- tests/test_ssl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index e0d1b999..aa6a253f 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3157,7 +3157,7 @@ def _fill_client_buffer(self, client_socket: socket) -> None: def _attempt_want_write_error(self, client: Connection) -> int: """ Attempts to send application data over SSL to trigger WantWriteError. - Returns (True, successful_size) if triggered, + Returns successful_size if triggered, otherwise calls pytest.fail. """ print("--- Phase 2: Attempting to trigger WantWriteError ---") From b3fb213a7d6ee9599012b13bdeeafcd533214db7 Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 21 Jul 2025 23:15:39 -0700 Subject: [PATCH 32/44] fixed wording on _attempt_want_write_error --- tests/test_ssl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index a096b245..4c739c04 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3158,7 +3158,7 @@ def _fill_client_buffer(self, client_socket: socket) -> None: def _attempt_want_write_error(self, client: Connection) -> int: """ Attempts to send application data over SSL to trigger WantWriteError. - Returns (True, successful_size) if triggered, + Returns successful_size if triggered, otherwise calls pytest.fail. """ print("--- Phase 2: Attempting to trigger WantWriteError ---") From ef258c63d4c9ac600883b937687cb2b8c14fcdf7 Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 27 Jul 2025 14:33:45 -0700 Subject: [PATCH 33/44] revised create_ssl_nonblocking_connection() to resuse _create_certificate_chain() --- tests/test_ssl.py | 51 +++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 4c739c04..a9b49a70 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -445,43 +445,42 @@ def create_ssl_nonblocking_connection( """ Create a pair of sockets and set up an SSL connection between them. """ - # Create a private key and a certificate to use for the server - key = PKey() - key.generate_key(TYPE_RSA, 2048) - cert = X509() - cert.set_version(2) - cert.get_subject().C = b"US" - cert.get_subject().ST = b"California" - cert.get_subject().L = b"Palo Alto" - cert.get_subject().O = b"pyOpenSSL" - cert.get_subject().CN = b"localhost" - cert.set_serial_number(1) - cert.gmtime_adj_notBefore(0) - cert.gmtime_adj_notAfter(60 * 60) - cert.set_issuer(cert.get_subject()) - cert.set_pubkey(key) - cert.sign(key, "sha1") - - # Create a context with the necessary modes - ctx = Context(SSLv23_METHOD) + chain = _create_certificate_chain() + + # Extract the server's key and certificate from the chain --- + # The chain is [ (root_key, root_cert), (intermediate_key, intermediate_cert), (server_key, server_cert) ] + server_key, server_cert = chain[2] # Index 2 gets the last tuple: (skey, scert) + + # Set up the server's SSL context --- + server_ctx = Context(SSLv23_METHOD) + server_ctx.use_privatekey(server_key) # Use the server_key from the chain + server_ctx.use_certificate(server_cert) # Use the server_cert from the chain + server_ctx.add_extra_chain_cert(chain[1][1]) # Add the intermediate cert to the server's extra chain + + # Set up client context + client_ctx = Context(SSLv23_METHOD) # these modes are set by default when ctx is initialized # clear them so we can run tests with or without them - ctx.clear_mode( + client_ctx.clear_mode( _lib.SSL_MODE_ENABLE_PARTIAL_WRITE | _lib.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER ) + client_ctx.set_mode(mode) - ctx.set_mode(mode) - ctx.use_privatekey(key) - ctx.use_certificate(cert) + # Get the certificate store from the context + cert_store = client_ctx.get_cert_store() + # Add the Root CA certificate to the store + cert_store.add_cert(chain[0][1]) # chain[0][1] is the pyOpenSSL X509 object for the root CA + # Enable peer verification so the client actually checks the server's cert + client_ctx.set_verify(SSL.VERIFY_PEER, lambda conn, cert, errnum, depth, ok: ok) # Create connections with real sockets client_socket, server_socket = socket_pair() # Create Connection objects from the sockets - client = Connection(ctx, client_socket) - server = Connection(ctx, server_socket) + client = Connection(client_ctx, client_socket) + server = Connection(server_ctx, server_socket) # Set the buffers to be very small so we can easily fill them client_socket.setsockopt(SOL_SOCKET, SO_SNDBUF, 256) @@ -3287,7 +3286,7 @@ def _perform_moving_buffer_test( except SSL.Error as e: reason = get_ssl_error_reason(e) if reason == "bad write retry": - print(f"Got expected SSL error: {e} ({reason}).") + print(f"Got SSL error: {e} ({reason}).") return True # Bad write retry else: pytest.fail( From 994338d6a6c2c69515a45aa28a6862020df85c24 Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 27 Jul 2025 14:46:39 -0700 Subject: [PATCH 34/44] formatting fixes --- tests/test_ssl.py | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index a9b49a70..983cbc4c 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -448,14 +448,21 @@ def create_ssl_nonblocking_connection( chain = _create_certificate_chain() # Extract the server's key and certificate from the chain --- - # The chain is [ (root_key, root_cert), (intermediate_key, intermediate_cert), (server_key, server_cert) ] - server_key, server_cert = chain[2] # Index 2 gets the last tuple: (skey, scert) + # The chain is [ (root_key, root_cert), + # (intermediate_key, intermediate_cert), (server_key, server_cert) ] + server_key, server_cert = chain[ + 2 + ] # Index 2 gets the last tuple: (skey, scert) # Set up the server's SSL context --- server_ctx = Context(SSLv23_METHOD) - server_ctx.use_privatekey(server_key) # Use the server_key from the chain - server_ctx.use_certificate(server_cert) # Use the server_cert from the chain - server_ctx.add_extra_chain_cert(chain[1][1]) # Add the intermediate cert to the server's extra chain + server_ctx.use_privatekey(server_key) # Use the server_key from the chain + server_ctx.use_certificate( + server_cert + ) # Use the server_cert from the chain + server_ctx.add_extra_chain_cert( + chain[1][1] + ) # Add the intermediate cert to the server's extra chain # Set up client context client_ctx = Context(SSLv23_METHOD) @@ -470,10 +477,20 @@ def create_ssl_nonblocking_connection( # Get the certificate store from the context cert_store = client_ctx.get_cert_store() + + # Assert that cert_store is not None to satisfy mypy + assert cert_store is not None, ( + "Expected X509Store, but got None from get_cert_store()" + ) + # Add the Root CA certificate to the store - cert_store.add_cert(chain[0][1]) # chain[0][1] is the pyOpenSSL X509 object for the root CA + cert_store.add_cert( + chain[0][1] + ) # chain[0][1] is the pyOpenSSL X509 object for the root CA # Enable peer verification so the client actually checks the server's cert - client_ctx.set_verify(SSL.VERIFY_PEER, lambda conn, cert, errnum, depth, ok: ok) + client_ctx.set_verify( + SSL.VERIFY_PEER, lambda conn, cert, errnum, depth, ok: bool(ok) + ) # Create connections with real sockets client_socket, server_socket = socket_pair() From ba9ba3f5e150c1a25bbd44c27443529de016cc9c Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 28 Jul 2025 12:37:52 -0700 Subject: [PATCH 35/44] undid the change to test_wantWriteErro --- tests/test_ssl.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 983cbc4c..a3ac034c 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3125,13 +3125,12 @@ def test_wantWriteError(self) -> None: # signal a short write via its return value it seems this doesn't # always happen on all platforms (FreeBSD and OS X particular) for the # very last bit of available buffer space. - for msg in [b"x" * 65536, b"x" * 16, b"x"]: + for msg in [b"x" * 65536, b"x"]: for i in range(1024 * 1024 * 64): try: client_socket.send(msg) except OSError as e: if e.errno == EWOULDBLOCK: - time.sleep(0.1) break raise # pragma: no cover else: # pragma: no cover From c48e628f93f1d4b1cac1289440268be3eb53382a Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 3 Aug 2025 14:55:10 -0700 Subject: [PATCH 36/44] simplified and revised moving buffer tests --- tests/test_ssl.py | 289 ++++++++++++++++++++++++++-------------------- 1 file changed, 163 insertions(+), 126 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index a3ac034c..d67ebb95 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -9,6 +9,7 @@ import datetime import gc +import logging import os import pathlib import select @@ -140,6 +141,8 @@ ) from .util import NON_ASCII, WARNING_TYPE_EXPECTED +logger = logging.getLogger(__name__) + # openssl dhparam 2048 -out dh-2048.pem dhparam = """\ -----BEGIN DH PARAMETERS----- @@ -440,10 +443,12 @@ def get_ssl_error_reason(ssl_error: SSL.Error) -> str | None: def create_ssl_nonblocking_connection( - mode: int, -) -> tuple[socket, socket, Connection, Connection]: + mode: int | None, request_send_buffer_size: int +) -> tuple[socket, socket, Connection, Connection, int, int]: """ Create a pair of sockets and set up an SSL connection between them. + mode: The mode to set if not None. + Returns the raw sockets and the SSL Connection objects. """ chain = _create_certificate_chain() @@ -467,13 +472,18 @@ def create_ssl_nonblocking_connection( # Set up client context client_ctx = Context(SSLv23_METHOD) - # these modes are set by default when ctx is initialized - # clear them so we can run tests with or without them - client_ctx.clear_mode( - _lib.SSL_MODE_ENABLE_PARTIAL_WRITE - | _lib.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER - ) - client_ctx.set_mode(mode) + # SSL_MODE_ENABLE_PARTIAL_WRITE and + # SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER modes + # are set by default when ctx is initialized. + # Clear them if requested so tests can + # be run without them if so desired. + if mode is not None: + client_ctx.clear_mode( + _lib.SSL_MODE_ENABLE_PARTIAL_WRITE + | _lib.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER + ) + # Set the new mode to the requested value + client_ctx.set_mode(mode) # Get the certificate store from the context cert_store = client_ctx.get_cert_store() @@ -499,9 +509,25 @@ def create_ssl_nonblocking_connection( client = Connection(client_ctx, client_socket) server = Connection(server_ctx, server_socket) - # Set the buffers to be very small so we can easily fill them - client_socket.setsockopt(SOL_SOCKET, SO_SNDBUF, 256) - server_socket.setsockopt(SOL_SOCKET, SO_RCVBUF, 128) + # Set the buffers to be small so we can easily fill them + # although the OS may not respect the values. + # Make the receive buffer smaller than the send buffer. + requested_receive_buffer_size = request_send_buffer_size // 2 + client_socket.setsockopt(SOL_SOCKET, SO_SNDBUF, request_send_buffer_size) + actual_sndbuf = client_socket.getsockopt(SOL_SOCKET, SO_SNDBUF) + logger.debug( + f"Attempted SO_SNDBUF: {request_send_buffer_size}, " + f"Actual SO_SNDBUF: {actual_sndbuf}" + ) + + server_socket.setsockopt( + SOL_SOCKET, SO_RCVBUF, requested_receive_buffer_size + ) + actual_rcvbuf = server_socket.getsockopt(SOL_SOCKET, SO_RCVBUF) + logger.debug( + f"Attempted SO_RCVBUF: {requested_receive_buffer_size}, " + f"Actual SO_RCVBUF: {actual_rcvbuf}" + ) # Manually set the connection state client.set_connect_state() @@ -560,7 +586,14 @@ def create_ssl_nonblocking_connection( if not (client_handshake_done and server_handshake_done): raise Exception("SSL handshake failed to complete") - return client_socket, server_socket, client, server + return ( + client_socket, + server_socket, + client, + server, + actual_sndbuf, + actual_rcvbuf, + ) class TestVersion: @@ -3147,78 +3180,49 @@ def test_wantWriteError(self) -> None: # XXX want_read - def _fill_client_buffer(self, client_socket: socket) -> None: - """ - Attempts to fill the client's raw send buffer until - EWOULDBLOCK is hit. - """ - print("--- Phase 1: Filling client socket buffer ---") - for msg in [b"x" * 65536, b"x" * 16]: - for _ in range( - 1024 * 1024 * 64 - ): # Large loop count to ensure buffer fill - try: - client_socket.send(msg) - time.sleep(0.01) - except OSError as e: - if e.errno == EWOULDBLOCK: - print("Client socket buffer filled (EWOULDBLOCK hit).") - return # Buffer successfully filled, exit function - raise # pragma: no cover # Re-raise unexpected OSErrors - else: # If the inner loop completes without hitting EWOULDBLOCK - pytest.fail( - "Failed to fill socket buffer, cannot test bad write error" - ) # pragma: no cover - - def _attempt_want_write_error(self, client: Connection) -> int: + def _attempt_want_write_error( + self, client: Connection, buffer_size: int + ) -> bytes: """ Attempts to send application data over SSL to trigger WantWriteError. Returns successful_size if triggered, otherwise calls pytest.fail. """ - print("--- Phase 2: Attempting to trigger WantWriteError ---") - test_sizes = [ - 64, - 128, - 256, - 512, - 1024, - 2048, - 4096, - 8192, - 16384, - 32768, - 65536, - ] + logger.debug("--- Phase 1: Attempting to trigger WantWriteError ---") initial_want_write_triggered = False - successful_size = -1 + max_num_of_attempts = 100000 - for size in test_sizes: - msg2 = b"Y" * size + for i in range(max_num_of_attempts): + msg = b"Y" * buffer_size try: - client.send(msg2) - # Continue loop to try larger sizes until an error is hit + client.send(msg) + logger.debug( + "_attempt_want_write_error() trying " + f"to send {i + 1}th message" + ) except SSL.WantWriteError: - print( - f"Got WantWriteError with message size {size} " - "(this is what we want)." + logger.debug( + f"After {i + 1} attempt(s) successfully induced a " + f"WantWriteError with message size {buffer_size}. " + "Buffer location in _attempt_want_write_error() " + f"is {id(msg):#x}" ) initial_want_write_triggered = True - successful_size = size * 2 # double it to be really sure break # Exit loop as desired error was triggered + except Exception as e: # pragma: no cover + error_string = str(e) + logger.debug(f"Attempt {i} failed with error: {error_string}") if not initial_want_write_triggered: - pytest.fail( - "Could not induce WantWriteError with any message size." - ) # pragma: no cover + pytest.fail("Could not induce WantWriteError") # pragma: no cover - return successful_size + return msg def _drain_server_buffers( self, server: Connection, server_socket: socket ) -> None: """Reads from server SSL and raw sockets to drain any pending data.""" - print("--- Phase 3: Draining server buffers ---") + logger.debug("--- Phase 2: Draining server buffers ---") total_read = 0 read_chunks = [] @@ -3226,8 +3230,13 @@ def _drain_server_buffers( # First, try to read any SSL data that might be available try: server.recv(65536) - except (SSL.WantReadError, SSL.Error) as ssl_error: - print(f"No SSL data available or SSL error: {ssl_error}") + except ( + SSL.WantReadError, + SSL.Error, + ) as ssl_error: # pragma: no cover + logger.debug( + f"No SSL data available or SSL error: {ssl_error}" + ) # Now read raw data from the underlying server socket to # drain buffer @@ -3238,11 +3247,11 @@ def _drain_server_buffers( total_read < 1024 * 1024 ): # Read up to 1MB or until no more data try: - data = server_socket.recv(65536) # Read raw data + data = server_socket.recv(65536) read_chunks.append(data) total_read += len(data) - print( - f"Read {len(data)} bytes of raw data from " + logger.debug( + f"Read {len(data)} bytes of data from " f"server socket (total: {total_read})." ) time.sleep(0.01) # Small delay between reads @@ -3250,63 +3259,64 @@ def _drain_server_buffers( except OSError as e: if e.errno == EWOULDBLOCK: consecutive_empty_reads += 1 - if consecutive_empty_reads >= 5: - print( - "No more raw data available from server " - "socket after retries." + if consecutive_empty_reads >= 10: + logger.debug( + "No more data available from server socket " + f"after {consecutive_empty_reads} retries." ) break - print( + logger.debug( "No data available, waiting... " f"(attempt {consecutive_empty_reads})." ) time.sleep(0.1) # Wait longer when buffer is empty continue else: # pragma: no cover - print(f"OSError while reading from server socket: {e}") + logger.error( + f"OSError while reading from server socket: {e}" + ) break - print( - f"Finished reading from server. Total bytes read: {total_read}" + logger.debug( + f"Finished reading from server. Bytes read: {total_read}. " ) - print("Allowing network buffers to settle...") + # Allow network buffers to settle time.sleep(0.1) except Exception as read_exception: # pragma: no cover - print(f"Exception while reading from server: {read_exception}") + logger.error(f"Exception reading from server: {read_exception}") def _perform_moving_buffer_test( - self, client: Connection, successful_size: int + self, client: Connection, buffer_size: int, want_bad_retry: bool ) -> bool: """ Attempts a retry write with a moving buffer and checks for 'bad write retry' error. Returns True if 'bad write retry' occurs, False otherwise. """ - print("--- Phase 4: Performing moving buffer retry test ---") - assert successful_size > -1, ( - "successful_size must be greater than -1 for a WantWriteError " - "to be triggered" - ) - msg3 = b"Z" * successful_size - - print( - "Attempting retry with different buffer " - f"(same size {successful_size})." - ) + logger.debug("Phase 3: Performing moving buffer retry test") + # Attempt retry with different buffer but same size + msg2 = b"Z" * buffer_size + logger.debug(f"buffer location for msg3 is {id(msg2):#x}") try: - client.send(msg3) - print(f"Retry succeeded with {successful_size} bytes written.") + bytes_written = client.send(msg2) + if want_bad_retry: + logger.debug( + "_perform_moving_buffer_test() failed as retry succeeded " + f"unexpectedly with {bytes_written} bytes written." + ) # pragma: no cover return False # Retry succeeded except SSL.Error as e: reason = get_ssl_error_reason(e) if reason == "bad write retry": - print(f"Got SSL error: {e} ({reason}).") + logger.debug(f"Got SSL error: {e!r} ({reason}).") return True # Bad write retry else: + logger.debug(f"Got SSL error: {e!r} ({reason}).") pytest.fail( - f"Retry failed with unexpected SSL error: {e} ({reason})." + f"Retry failed with unexpected SSL error: {e!r} " + f"({reason})." ) # pragma: no cover # If any other exception occurs, it will propagate up @@ -3318,46 +3328,64 @@ def _shutdown_connections( server_socket: socket, ) -> None: """Helper to safely shut down SSL connections and close sockets.""" - print("--- Cleanup: Shutting down connections ---") + logger.debug("--- Cleanup: Shutting down connections ---") try: if client: client.shutdown() - except Exception as e: - print(f"Error during client SSL shutdown: {e}") + except Exception: + # An exception is usually thrown so it is caught and ignored. + pass try: if server: server.shutdown() - except Exception as e: - print(f"Error during server SSL shutdown: {e}") + except Exception: # pragma: no cover + pass finally: if client_socket: client_socket.close() if server_socket: server_socket.close() - print("Connections closed.") + # Connections closed. - def _badwriteretry(self, mode: int) -> bool: - """ - Tries to force a "bad write retry" error over an SSL connection - by using a moving buffer. Returns True if a bad write retry - error occurs. + def _badwriteretry( + self, + want_bad_retry: bool, + modeflag: int | None, + ) -> bool: """ - client_socket, server_socket, client, server = ( - create_ssl_nonblocking_connection(mode) + Tests for a "bad write retry" error over an SSL connection + by using a moving buffer which is allowed by default with + SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER. If this mode is unset + we expect a bad write retry error when using a moving buffer. + want_bad_retry: If True, the caller expects a bad write retry error. + mode: If not None, unsets the defaults that include + SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER and replaces with the given mode. + Returns True if a bad write retry error occurs. + """ + client_socket, server_socket, client, server, sndbuf, rcvbuf = ( + create_ssl_nonblocking_connection(modeflag, 2048) ) result = False # Default return value + # set buffer size to the minimum of send and receive buffers + buffer_size = min(sndbuf, rcvbuf) // 2 try: # --- Main Test Flow --- - self._fill_client_buffer(client_socket) - - successful_size = self._attempt_want_write_error(client) - - # if WantWriteError was not triggered the test fails in - # _attempt_want_write_error(). - # proceed with draining and retry + # _attempt_want_write_error() terminates the test + # if WantWriteError is not triggered + # The function also returns the message that triggered + # the WantWriteError so that when we attempt a retry + # we can ensure a different buffer location is allocated + # to a the new message we will send for the retry. + _ = self._attempt_want_write_error(client, buffer_size) + + # proceed with draining so that a retry has a chance to succeed self._drain_server_buffers(server, server_socket) - result = self._perform_moving_buffer_test(client, successful_size) + + # now attempt the moving buffer retry + result = self._perform_moving_buffer_test( + client, buffer_size, want_bad_retry + ) except Exception as e: # pragma: no cover pytest.fail(f"Unexpected exception during test: {e}.") finally: @@ -3371,14 +3399,14 @@ def test_moving_write_buffer_should_pass(self) -> None: """ After an `OpenSSL.SSL.WantWriteError` if the SSL connection processed some data, the connection may expect a retry with the same buffer. - Using mode SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER makes it possible - to use a different buffer location provided the length is the same. + SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER is applied by default. This + makes it possible to use a different buffer location + for retries provided the length remains the same. """ - mode = ( - _lib.SSL_MODE_ENABLE_PARTIAL_WRITE - | _lib.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER - ) - result = self._badwriteretry(mode) + # Setting modeflag to None preserves the default mode + modeflag = None + want_bad_retry = False + result = self._badwriteretry(want_bad_retry, modeflag) assert result is False, ( "Using SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER failed to prevent bad " @@ -3393,8 +3421,17 @@ def test_moving_write_buffer_should_fail(self) -> None: should generate a bad write retry error if a different buffer is presented. """ - mode = 0 - result = self._badwriteretry(mode) + # We want to trigger a bad write retry error for this test to succeed + # Without SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER + # it should not be possible to retry with a different buffer + # location + want_bad_retry = True + + # passing a replacement mode value will ensure that + # SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER is unset while the + # default mode SSL_MODE_ENABLE_PARTIAL_WRITE remains. + mode = _lib.SSL_MODE_ENABLE_PARTIAL_WRITE + result = self._badwriteretry(want_bad_retry, mode) assert result is True, ( "Use of a moving buffer without " From df909eab8245183ac3f56ff9917fbabf0039ff92 Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 3 Aug 2025 16:19:10 -0700 Subject: [PATCH 37/44] revised create_ssl_nonblocking_connection() to use handshake() --- tests/test_ssl.py | 64 ++++++----------------------------------------- 1 file changed, 7 insertions(+), 57 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index d67ebb95..3166e494 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -448,7 +448,9 @@ def create_ssl_nonblocking_connection( """ Create a pair of sockets and set up an SSL connection between them. mode: The mode to set if not None. - Returns the raw sockets and the SSL Connection objects. + request_send_buffer_size: requested size of the send buffer + Returns the raw sockets, the SSL Connection objects + and the actual send/receive buffer sizes. """ chain = _create_certificate_chain() @@ -509,8 +511,8 @@ def create_ssl_nonblocking_connection( client = Connection(client_ctx, client_socket) server = Connection(server_ctx, server_socket) - # Set the buffers to be small so we can easily fill them - # although the OS may not respect the values. + # Allow caller to request small buffer sizes so they can be easily filled. + # Note the OS may not respect the requested values. # Make the receive buffer smaller than the send buffer. requested_receive_buffer_size = request_send_buffer_size // 2 client_socket.setsockopt(SOL_SOCKET, SO_SNDBUF, request_send_buffer_size) @@ -519,7 +521,6 @@ def create_ssl_nonblocking_connection( f"Attempted SO_SNDBUF: {request_send_buffer_size}, " f"Actual SO_SNDBUF: {actual_sndbuf}" ) - server_socket.setsockopt( SOL_SOCKET, SO_RCVBUF, requested_receive_buffer_size ) @@ -529,63 +530,12 @@ def create_ssl_nonblocking_connection( f"Actual SO_RCVBUF: {actual_rcvbuf}" ) - # Manually set the connection state + # set the connection state client.set_connect_state() server.set_accept_state() - # Perform the handshake with proper completion detection - client_handshake_done = False - server_handshake_done = False - max_handshake_attempts = 100 # Prevent infinite loops - attempts = 0 - while ( - not (client_handshake_done and server_handshake_done) - and attempts < max_handshake_attempts - ): - attempts += 1 - # Try client handshake - if not client_handshake_done: - try: - client.do_handshake() - client_handshake_done = True - except SSL.WantReadError: - # Client needs to read data - pass - except SSL.WantWriteError: - # Client needs to write data - pass - - # Try server handshake - if not server_handshake_done: - try: - server.do_handshake() - server_handshake_done = True - except SSL.WantReadError: - # Server needs to read data - pass - except SSL.WantWriteError: - # Server needs to write data - pass - - # If neither handshake is complete, wait for socket activity - if not (client_handshake_done and server_handshake_done): - # Use select to wait for socket activity - ready_read, ready_write, ready_err = select.select( - [client_socket, server_socket], - [client_socket, server_socket], - [client_socket, server_socket], - 1.0, # 1 second timeout - ) - - if ready_err: - raise Exception(f"Socket error during handshake: {ready_err}") - - if not (ready_read or ready_write): - # Timeout occurred, but continue trying - continue + handshake(client, server) - if not (client_handshake_done and server_handshake_done): - raise Exception("SSL handshake failed to complete") return ( client_socket, server_socket, From c7ee91e8e083b92f20677fdfd290faa645dcf9fa Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 3 Aug 2025 17:30:16 -0700 Subject: [PATCH 38/44] adjusted requested buffer size --- tests/test_ssl.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 3166e494..b4163f04 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -521,6 +521,7 @@ def create_ssl_nonblocking_connection( f"Attempted SO_SNDBUF: {request_send_buffer_size}, " f"Actual SO_SNDBUF: {actual_sndbuf}" ) + server_socket.setsockopt( SOL_SOCKET, SO_RCVBUF, requested_receive_buffer_size ) @@ -3312,8 +3313,9 @@ def _badwriteretry( SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER and replaces with the given mode. Returns True if a bad write retry error occurs. """ + request_buffer_size = 4096 # Size of the send buffer we'll request client_socket, server_socket, client, server, sndbuf, rcvbuf = ( - create_ssl_nonblocking_connection(modeflag, 2048) + create_ssl_nonblocking_connection(modeflag, request_buffer_size) ) result = False # Default return value # set buffer size to the minimum of send and receive buffers From 3d0dd05f4ae364b96b18b630c43ee527b7d9beb8 Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 3 Aug 2025 19:04:24 -0700 Subject: [PATCH 39/44] simplified create_ssl_nonblocking_connection() further --- tests/test_ssl.py | 46 +++++----------------------------------------- 1 file changed, 5 insertions(+), 41 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index b4163f04..a380ddfd 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -452,24 +452,8 @@ def create_ssl_nonblocking_connection( Returns the raw sockets, the SSL Connection objects and the actual send/receive buffer sizes. """ - chain = _create_certificate_chain() - - # Extract the server's key and certificate from the chain --- - # The chain is [ (root_key, root_cert), - # (intermediate_key, intermediate_cert), (server_key, server_cert) ] - server_key, server_cert = chain[ - 2 - ] # Index 2 gets the last tuple: (skey, scert) - - # Set up the server's SSL context --- - server_ctx = Context(SSLv23_METHOD) - server_ctx.use_privatekey(server_key) # Use the server_key from the chain - server_ctx.use_certificate( - server_cert - ) # Use the server_cert from the chain - server_ctx.add_extra_chain_cert( - chain[1][1] - ) # Add the intermediate cert to the server's extra chain + + client_socket, server_socket = socket_pair() # Set up client context client_ctx = Context(SSLv23_METHOD) @@ -487,29 +471,9 @@ def create_ssl_nonblocking_connection( # Set the new mode to the requested value client_ctx.set_mode(mode) - # Get the certificate store from the context - cert_store = client_ctx.get_cert_store() - - # Assert that cert_store is not None to satisfy mypy - assert cert_store is not None, ( - "Expected X509Store, but got None from get_cert_store()" - ) - - # Add the Root CA certificate to the store - cert_store.add_cert( - chain[0][1] - ) # chain[0][1] is the pyOpenSSL X509 object for the root CA - # Enable peer verification so the client actually checks the server's cert - client_ctx.set_verify( - SSL.VERIFY_PEER, lambda conn, cert, errnum, depth, ok: bool(ok) - ) - - # Create connections with real sockets - client_socket, server_socket = socket_pair() - - # Create Connection objects from the sockets + # create the SSL connections client = Connection(client_ctx, client_socket) - server = Connection(server_ctx, server_socket) + server = loopback_server_factory(server_socket) # Allow caller to request small buffer sizes so they can be easily filled. # Note the OS may not respect the requested values. @@ -533,7 +497,7 @@ def create_ssl_nonblocking_connection( # set the connection state client.set_connect_state() - server.set_accept_state() + # loopback_server_factory already sets the accept state on the server handshake(client, server) From 5cd4eb3f753198849d8d66cbd76a339aa0be53bc Mon Sep 17 00:00:00 2001 From: julianz- Date: Sun, 3 Aug 2025 19:14:57 -0700 Subject: [PATCH 40/44] adjusted buffer size --- tests/test_ssl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index a380ddfd..21035cbf 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3277,7 +3277,7 @@ def _badwriteretry( SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER and replaces with the given mode. Returns True if a bad write retry error occurs. """ - request_buffer_size = 4096 # Size of the send buffer we'll request + request_buffer_size = 65536 # Size of the send buffer we'll request client_socket, server_socket, client, server, sndbuf, rcvbuf = ( create_ssl_nonblocking_connection(modeflag, request_buffer_size) ) From 44a371aa440c0aef9d729b455ceec651560a6c8e Mon Sep 17 00:00:00 2001 From: julianz- Date: Mon, 4 Aug 2025 00:31:40 -0700 Subject: [PATCH 41/44] simplified _drain_server_buffers() --- tests/test_ssl.py | 86 +++++++++++++++++++---------------------------- 1 file changed, 34 insertions(+), 52 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 21035cbf..d1abe11e 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3138,68 +3138,50 @@ def _drain_server_buffers( ) -> None: """Reads from server SSL and raw sockets to drain any pending data.""" logger.debug("--- Phase 2: Draining server buffers ---") - total_read = 0 - read_chunks = [] + total_ssl_read = 0 + consecutive_empty_ssl_reads = 0 - try: - # First, try to read any SSL data that might be available + while total_ssl_read < 1024 * 1024: try: - server.recv(65536) - except ( - SSL.WantReadError, - SSL.Error, - ) as ssl_error: # pragma: no cover + data = server.recv(65536) + if not data: # Peer closed or empty read for some reason + logger.debug( + "SSL peer closed or empty data, stopping SSL drain." + ) + break + total_ssl_read += len(data) logger.debug( - f"No SSL data available or SSL error: {ssl_error}" + f"Read {len(data)} bytes of SSL application data " + f"(total: {total_ssl_read})." ) + # Reset counter on successful read + consecutive_empty_ssl_reads = 0 - # Now read raw data from the underlying server socket to - # drain buffer - server_socket.setblocking(False) # Ensure non-blocking - consecutive_empty_reads = 0 - - while ( - total_read < 1024 * 1024 - ): # Read up to 1MB or until no more data - try: - data = server_socket.recv(65536) - read_chunks.append(data) - total_read += len(data) + except SSL.WantReadError: + consecutive_empty_ssl_reads += 1 + if consecutive_empty_ssl_reads >= 10: logger.debug( - f"Read {len(data)} bytes of data from " - f"server socket (total: {total_read})." + "No more SSL application data available after " + f"{consecutive_empty_ssl_reads} retries." ) - time.sleep(0.01) # Small delay between reads + break + logger.debug( + "No SSL data available, waiting... " + f"(attempt {consecutive_empty_ssl_reads})." + ) + time.sleep(0.01) # Small delay for non-blocking SSL reads - except OSError as e: - if e.errno == EWOULDBLOCK: - consecutive_empty_reads += 1 - if consecutive_empty_reads >= 10: - logger.debug( - "No more data available from server socket " - f"after {consecutive_empty_reads} retries." - ) - break - logger.debug( - "No data available, waiting... " - f"(attempt {consecutive_empty_reads})." - ) - time.sleep(0.1) # Wait longer when buffer is empty - continue - else: # pragma: no cover - logger.error( - f"OSError while reading from server socket: {e}" - ) - break + except SSL.Error as ssl_error: + logger.debug(f"SSL error during drain: {ssl_error}") + break # Stop on SSL protocol errors + except Exception as e: # Catch other potential errors + logger.error(f"Unexpected error during SSL drain: {e}") + break - logger.debug( - f"Finished reading from server. Bytes read: {total_read}. " - ) - # Allow network buffers to settle - time.sleep(0.1) + logger.debug(f"Finished draining SSL. Bytes read: {total_ssl_read}.") - except Exception as read_exception: # pragma: no cover - logger.error(f"Exception reading from server: {read_exception}") + # Allow network buffers to settle + time.sleep(0.1) def _perform_moving_buffer_test( self, client: Connection, buffer_size: int, want_bad_retry: bool From dd61474d15b336588bc1b90d4d4f15a042068571 Mon Sep 17 00:00:00 2001 From: julianz- Date: Thu, 7 Aug 2025 11:06:21 -0700 Subject: [PATCH 42/44] remove no cover from _badwriteretry --- .github/workflows/ci.yml | 2 +- tests/test_ssl.py | 42 ++++++++++++++++++---------------------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11e5f9fd..6f86e203 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: with: python-version: ${{ matrix.PYTHON.VERSION }} - run: python -m pip install tox - - run: tox -v + - run: tox -v -- --log-cli-level=DEBUG --show-capture=all env: TOXENV: ${{ matrix.PYTHON.TOXENV }} - uses: ./.github/actions/upload-coverage diff --git a/tests/test_ssl.py b/tests/test_ssl.py index d1abe11e..575d42bf 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3267,29 +3267,25 @@ def _badwriteretry( # set buffer size to the minimum of send and receive buffers buffer_size = min(sndbuf, rcvbuf) // 2 - try: - # --- Main Test Flow --- - # _attempt_want_write_error() terminates the test - # if WantWriteError is not triggered - # The function also returns the message that triggered - # the WantWriteError so that when we attempt a retry - # we can ensure a different buffer location is allocated - # to a the new message we will send for the retry. - _ = self._attempt_want_write_error(client, buffer_size) - - # proceed with draining so that a retry has a chance to succeed - self._drain_server_buffers(server, server_socket) - - # now attempt the moving buffer retry - result = self._perform_moving_buffer_test( - client, buffer_size, want_bad_retry - ) - except Exception as e: # pragma: no cover - pytest.fail(f"Unexpected exception during test: {e}.") - finally: - self._shutdown_connections( - client, server, client_socket, server_socket - ) + # --- Main Test Flow --- + # _attempt_want_write_error() terminates the test + # if WantWriteError is not triggered + # The function also returns the message that triggered + # the WantWriteError so that when we attempt a retry + # we can ensure a different buffer location is allocated + # to a the new message we will send for the retry. + _ = self._attempt_want_write_error(client, buffer_size) + + # proceed with draining so that a retry has a chance to succeed + self._drain_server_buffers(server, server_socket) + + # now attempt the moving buffer retry + result = self._perform_moving_buffer_test( + client, buffer_size, want_bad_retry + ) + self._shutdown_connections( + client, server, client_socket, server_socket + ) return result From 4b42b89c45f30d550b080f8bc1f24a6dacb5c473 Mon Sep 17 00:00:00 2001 From: julianz- Date: Thu, 7 Aug 2025 15:24:06 -0700 Subject: [PATCH 43/44] removed some code with pragma no covers --- tests/test_ssl.py | 51 +++++++++++++++-------------------------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 575d42bf..99b16738 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3124,12 +3124,8 @@ def _attempt_want_write_error( ) initial_want_write_triggered = True break # Exit loop as desired error was triggered - except Exception as e: # pragma: no cover - error_string = str(e) - logger.debug(f"Attempt {i} failed with error: {error_string}") - if not initial_want_write_triggered: - pytest.fail("Could not induce WantWriteError") # pragma: no cover + assert initial_want_write_triggered, "Could not induce WantWriteError" return msg @@ -3138,6 +3134,7 @@ def _drain_server_buffers( ) -> None: """Reads from server SSL and raw sockets to drain any pending data.""" logger.debug("--- Phase 2: Draining server buffers ---") + total_ssl_read = 0 consecutive_empty_ssl_reads = 0 @@ -3171,17 +3168,9 @@ def _drain_server_buffers( ) time.sleep(0.01) # Small delay for non-blocking SSL reads - except SSL.Error as ssl_error: - logger.debug(f"SSL error during drain: {ssl_error}") - break # Stop on SSL protocol errors - except Exception as e: # Catch other potential errors - logger.error(f"Unexpected error during SSL drain: {e}") - break - - logger.debug(f"Finished draining SSL. Bytes read: {total_ssl_read}.") - - # Allow network buffers to settle - time.sleep(0.1) + logger.debug( + f"Finished reading from server. Bytes read: {total_ssl_read}. " + ) def _perform_moving_buffer_test( self, client: Connection, buffer_size: int, want_bad_retry: bool @@ -3195,27 +3184,21 @@ def _perform_moving_buffer_test( # Attempt retry with different buffer but same size msg2 = b"Z" * buffer_size - logger.debug(f"buffer location for msg3 is {id(msg2):#x}") + logger.debug(f"buffer location for msg2 is {id(msg2):#x}") try: bytes_written = client.send(msg2) - if want_bad_retry: - logger.debug( - "_perform_moving_buffer_test() failed as retry succeeded " - f"unexpectedly with {bytes_written} bytes written." - ) # pragma: no cover + assert not want_bad_retry, ( + "_perform_moving_buffer_test() failed as retry succeeded " + f"unexpectedly with {bytes_written} bytes written." + ) return False # Retry succeeded except SSL.Error as e: reason = get_ssl_error_reason(e) - if reason == "bad write retry": - logger.debug(f"Got SSL error: {e!r} ({reason}).") - return True # Bad write retry - else: - logger.debug(f"Got SSL error: {e!r} ({reason}).") - pytest.fail( - f"Retry failed with unexpected SSL error: {e!r} " - f"({reason})." - ) # pragma: no cover - # If any other exception occurs, it will propagate up + assert reason == "bad write retry", ( + f"Retry failed with unexpected SSL error: {e!r}({reason})." + ) + logger.debug(f"Got SSL error: {e!r} ({reason}).") + return True # Bad write retry def _shutdown_connections( self, @@ -3242,7 +3225,6 @@ def _shutdown_connections( client_socket.close() if server_socket: server_socket.close() - # Connections closed. def _badwriteretry( self, @@ -3264,7 +3246,7 @@ def _badwriteretry( create_ssl_nonblocking_connection(modeflag, request_buffer_size) ) result = False # Default return value - # set buffer size to the minimum of send and receive buffers + # set buffer size to half the minimum of send and receive buffers buffer_size = min(sndbuf, rcvbuf) // 2 # --- Main Test Flow --- @@ -3286,7 +3268,6 @@ def _badwriteretry( self._shutdown_connections( client, server, client_socket, server_socket ) - return result def test_moving_write_buffer_should_pass(self) -> None: From 92f5d210aef6a390291211aa50d85949c1199a67 Mon Sep 17 00:00:00 2001 From: julianz- Date: Thu, 7 Aug 2025 15:41:01 -0700 Subject: [PATCH 44/44] minor fix to tox.ini to not pass on pytest debug flags to sphinx --- tests/test_ssl.py | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 99b16738..fe428106 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3169,7 +3169,7 @@ def _drain_server_buffers( time.sleep(0.01) # Small delay for non-blocking SSL reads logger.debug( - f"Finished reading from server. Bytes read: {total_ssl_read}. " + f"Finished reading from server. Bytes read: {total_ssl_read}." ) def _perform_moving_buffer_test( diff --git a/tox.ini b/tox.ini index babaaed7..b48e766d 100644 --- a/tox.ini +++ b/tox.ini @@ -60,7 +60,7 @@ commands = extras = docs commands = - sphinx-build -W -b html doc doc/_build/html {posargs} + sphinx-build -W -b html doc doc/_build/html [testenv:coverage-report] deps = coverage[toml]>=4.2