Skip to content

Set SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER in calling OpenSSL#1287

Merged
alex merged 8 commits into
pyca:mainfrom
julianz-:jan23-2024
Aug 16, 2025
Merged

Set SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER in calling OpenSSL#1287
alex merged 8 commits into
pyca:mainfrom
julianz-:jan23-2024

Conversation

@julianz-

Copy link
Copy Markdown
Contributor

See cherrypy/cheroot#245 for discussion.

@mhils

mhils commented Jan 25, 2024

Copy link
Copy Markdown
Member

See #1242 for more context.

@mhils mhils left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pushing this further! 🍰 I think we had general agreement to do this in #1242, so this looks good to merge after some minor docs fixes. :)

We can ignore codecov, that seems to be a bug in determining coverage.

Comment thread CHANGELOG.rst Outdated
Comment thread CHANGELOG.rst Outdated
Comment thread CHANGELOG.rst Outdated
Comment thread tests/test_crypto.py
Comment thread src/OpenSSL/SSL.py Outdated
Comment thread CHANGELOG.rst
Comment thread CHANGELOG.rst

@webknjaz webknjaz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add some empty line separators under title marks.

Comment thread CHANGELOG.rst
@julianz- julianz- force-pushed the jan23-2024 branch 2 times, most recently from c2cae69 to 2e788b7 Compare January 26, 2024 23:48
@julianz- julianz- requested a review from webknjaz January 28, 2024 05:16
@julianz- julianz- marked this pull request as draft January 28, 2024 05:19
@julianz- julianz- marked this pull request as ready for review January 28, 2024 08:13
@julianz- julianz- marked this pull request as draft January 28, 2024 17:32
Comment thread setup.py Outdated
@julianz- julianz- force-pushed the jan23-2024 branch 2 times, most recently from 6c1d483 to 23e9e11 Compare January 28, 2024 19:07
@julianz- julianz- requested a review from webknjaz January 28, 2024 19:13
@julianz- julianz- marked this pull request as ready for review January 28, 2024 19:14
@webknjaz

Copy link
Copy Markdown
Contributor

@mhils @alex it appears that https://app.codecov.io/gh/pyca/pyopenssl treats master as the default branch, which is why it may get confused and assign coverage drops to pull requests that have nothing to do with said lines.

@alex

alex commented Jan 28, 2024

Copy link
Copy Markdown
Member

I've changed the default branch to be main

@webknjaz

Copy link
Copy Markdown
Contributor

Is there a smoke test we could include in this PR?

@julianz-

julianz- commented Jan 29, 2024

Copy link
Copy Markdown
Contributor Author

@webknjaz The only test for setting the mode currently is:

def test_set_mode(self):
        """
        `Context.set_mode` accepts a mode bitvector and returns the
        newly set mode.
        """
        context = Context(SSLv23_METHOD)
        assert MODE_RELEASE_BUFFERS & context.set_mode(MODE_RELEASE_BUFFERS)

This checks that setting the mode for MODE_RELEASE_BUFFERS returns the same bit. I guess we could add another check to make sure passing SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER also returns the appropriate value? Not sure whether that counts as a smoke test but it's something perhaps?

@webknjaz

Copy link
Copy Markdown
Contributor

Maybe, find some existing test where a write buffer is passed, copy it and pass a moving buffer there?

@mhils ideas?

@mhils

mhils commented Jan 29, 2024

Copy link
Copy Markdown
Member

We could possibly adapt something like this: https://github.com/pyca/pyopenssl/blob/main/tests/test_ssl.py#L2837

If that's easy to add I'm all for it, but I also feel that not having an elaborate test here is not the end of the world. There's precedent (SSL_MODE_ENABLE_PARTIAL_WRITE has no test either) and, more importantly, if this fails in the future we should get a very explicit 'bad write retry' error. Does CPython have a dedicated test for this?

This looks good to merge otherwise. @alex @reaperhulk, if you want to make a judgement call here please just merge. :)

@julianz-

Copy link
Copy Markdown
Contributor Author

@mhils Strangely, when I try running pytest locally on my branch I am getting an exception on that test that makes it fail the test:

E       OpenSSL.SSL.Error: [('system library', '', ''), ('system library', '', ''), ('system library', '', ''), ('system library', '', ''), ('SSL routines', '', 'certificate verify failed')]

../../../../Library/Python/3.9/lib/python/site-packages/OpenSSL/_util.py:57: Error
____________________________________________________________________ TestConnection.test_wantWriteError _____________________________________________________________________

That's just a fragment of the output so maybe not very meaningful but as I understand it, the test is meant to throw WantWriteError but for some reason although it's expected the exception is not being considered a success? How is this supposed to work?

@webknjaz

Copy link
Copy Markdown
Contributor

although it's expected the exception is not being considered a success?

In your log, a more generic exception happens (OpenSSL.SSL.Error), not OpenSSL.SSL.WantWriteError. This is why pytest.raises() doesn't match it as expected.

@julianz-

julianz- commented Feb 2, 2024

Copy link
Copy Markdown
Contributor Author

@webknjaz

In your log, a more generic exception happens (OpenSSL.SSL.Error), not OpenSSL.SSL.WantWriteError. This is why pytest.raises() doesn't match it as expected.

Ok to make any progress on this, I'm trying to understand the first test that is failing when I run pytest locally - test_set_default_verify_paths() in test_ssl.py. It's basically saying "certificate verify failed". I tried debugging this using the following in the command line:

openssl s_client -connect "encrypted.google.com":443

and get:

Verify return code: 18 (self signed certificate)

I assume this generates an error because the return code is not 0. But how is this supposed to work? The test relies on Google's certificate but there seems to be problem with the cert?

Comment thread tests/test_ssl.py
Comment thread tests/test_ssl.py Outdated
Comment thread tests/test_ssl.py Outdated
Comment thread tests/test_ssl.py
@julianz- julianz- requested a review from alex August 4, 2025 02:41
Comment thread tests/test_ssl.py Outdated
Comment thread tests/test_ssl.py Outdated
@julianz- julianz- requested a review from alex August 7, 2025 22:47

@alex alex left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very close! Thanks for your persistence on this.

I'm a bit ambivalent on the logs, it's not really our house style and IME for things like this, while the logs are useful in the initial development, they're unlikely to be useful down the line. (In contrast to logs in production software, where you have a more continious validation process.)

Comment thread .github/workflows/ci.yml Outdated
Comment thread tests/test_ssl.py Outdated
Comment thread tests/test_ssl.py Outdated
try:
if server:
server.shutdown()
except Exception: # pragma: no cover

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above

Comment thread tox.ini Outdated
Comment thread CHANGELOG.rst
Comment thread tests/test_ssl.py Outdated
Comment thread tests/test_ssl.py Outdated
Comment thread tests/test_ssl.py Outdated
Comment on lines +3229 to +3271
def _badwriteretry(
self,
want_bad_retry: bool,
modeflag: int | None,
) -> bool:
"""
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.
"""
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)
)
result = False # Default return value
# set buffer size to half the minimum of send and receive buffers
buffer_size = min(sndbuf, rcvbuf) // 2

# --- 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a sort of thing that seems natural to wrap with @contextlib.contextmanager or (even better @pytest.fixture with a yield for cleanup and indirect params).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion will do.

Comment thread tox.ini Outdated
…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.
@alex

alex commented Aug 16, 2025

Copy link
Copy Markdown
Member

There's some lint errors, and you'll need to update the minimum version in noxfile.py, but the code itself looks good. Thanks for your persistence herer!

@julianz- julianz- requested review from alex and webknjaz August 16, 2025 18:57

@alex alex left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@alex alex merged commit 5619943 into pyca:main Aug 16, 2025
39 checks passed
@julianz-

Copy link
Copy Markdown
Contributor Author

Thank you @alex and @webknjaz!

@webknjaz

Copy link
Copy Markdown
Contributor

Thanks! Hopefully, this will unblock Cheroot once released..

@julianz-

Copy link
Copy Markdown
Contributor Author

Thanks! Hopefully, this will unblock Cheroot once released.

Agreed! I wanted to create a PR for Cheroot once we had this.

@webknjaz

Copy link
Copy Markdown
Contributor

Thanks! Hopefully, this will unblock Cheroot once released.

Agreed! I wanted to create a PR for Cheroot once we had this.

It's been released a week ago. So that's finally unblocked!

@julianz-

Copy link
Copy Markdown
Contributor Author

Great thanks for the heads up!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

5 participants