-
Notifications
You must be signed in to change notification settings - Fork 419
Set SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER in calling OpenSSL #1287
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
See #1242 for more context. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
c2cae69
to
2e788b7
Compare
6c1d483
to
23e9e11
Compare
@mhils @alex it appears that https://app.codecov.io/gh/pyca/pyopenssl treats |
I've changed the default branch to be main |
Is there a smoke test we could include in this PR? |
@webknjaz The only test for setting the mode currently is:
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? |
Maybe, find some existing test where a write buffer is passed, copy it and pass a moving buffer there? @mhils ideas? |
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. :) |
@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:
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? |
In your log, a more generic exception happens ( |
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:
and get:
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? |
I am to get to it this weekend. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unclear why these tests are so complicated. As I understand the point of this flag, it's to allow passing a buffer of the same contents, but at a different address, which is needed when a buffer is filled up.
We have a test, test_wantWriteError
that shows filling up the buffer, why is it not sufficient to have that logic, then drain some of the buffer on the reader side, then perform another write?
tests/test_ssl.py
Outdated
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") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have utilities for generating certificates, this should use those.
In fact, we have utilities for generating connection pairs. Those should be extended, rather than duplicated.
test_wantWriteError is a very superficial test compared with the tests I added for the moving buffer because it only generates a WantWriteError during the handshaking phase. Generating a WantWriteError in this way is almost trivial compared with reliably generating a "bad write retry" error. A crucial element of the new tests is creating a full SSL socket connection with access to both the server and client and filling up the buffers at both ends. Once this is done, it's possible to send a message that partially succeeds but not completely so that it generates a WantWriteError and then expects a retry. test_wantWriteError won't do this at all. I needed to have handshaking completed and then application data to be sent. I had to spend a lot of time figuring out how to trigger bad write retries reliably and I recall seeing other comments that other people had tried and not succeeded. Some of the complication comes in probing how much application data can be sent before a WantWriteError occurs. If you simply send a huge buffer then you can easily get repeated WantWriteErrors but no bad retries. The buffer has to be large enough to trigger the first WantWriteError but small enough that you don't immediately get another WantWriteError after resending the same sized buffer (hence also the need to drain the buffers after the first WantWriteError so the second try has a chance of success). Finding the right size buffer depends on the environment so I had to make my code adaptive. Regarding your comments about creating the socket connection, I have slightly revised create_ssl_nonblocking_connection() to reuse an existing _create_certificate_chain() method rather than creating the certs from scratch. Incidentally, test_wantWriteError although it passes the CI on Github fails on my Mac usually generating a WantReadError rather than WantWriteError. I was able to fix the test to work on my Mac with a simple one line addition which still passed the CI but have not included the fix on this PR as this is a separate issue. However, this kind of error speaks to the difficulty and sensitivity of testing problems that arise from these kind of race conditions. |
tests/test_ssl.py
Outdated
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is never hit, it's better to just have assert e.errno == EWOULDBLOCK
. That way we have clear coverage and a clear assertion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok sure wlll replace.
tests/test_ssl.py
Outdated
) | ||
initial_want_write_triggered = True | ||
break # Exit loop as desired error was triggered | ||
except Exception as e: # pragma: no cover |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, we strongly avoid no cover pragmas. You can simply omit this handling, if some other exception is raised, you can just let it propagate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to know. Have revised to remove all but one of my no cover pragmas. The one remaining in _shutdown_connections() is forgivable I think but see what you think.
tests/test_ssl.py
Outdated
if not initial_want_write_triggered: | ||
pytest.fail("Could not induce WantWriteError") # pragma: no cover | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly, this can simply be assert initial_want_write_triggered
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
See cherrypy/cheroot#245 for discussion.