You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
SSLSocket.send() returns 0 for non-blocking socket #65150
When using non-blocking operation, the SSLSocket.send method returns 0 if no data can be sent at this point.
This is counterintuitive, because in the same situation (write to non-blocking socket that isn't ready for IO):
A regular (non-SSL) socket raises BlockingIOError
libc's send(2) does not return 0, but -EAGAIN or -EWOULDBLOCK.
OpenSSL's ssl_write does not return 0, but returns an SSL_ERROR_WANT_WRITE error
The ssl module's documentation describes the SSLWantWrite exception as "A subclass of SSLError raised by a non-blocking SSL socket when trying to read or write data, but more data needs to be sent on the underlying TCP transport before the request can be fulfilled."
Consistent with that, trying to *read* from a non-blocking SSLSocket when no data is ready raises SSLWantRead, instead of returning zero.
This behavior also makes it more complicated to write code that works with both SSLSockets and regular sockets.
Since the current behavior undocumented at best (and contradicting the documentation at worst), can we change this in Python 3.5?
This is actually seems to be not just an inconvience, but a real bug: since SSLSocket.sendall() uses SSLSocket.send() internally, the former method will busy-loop when called on a non-blocking socket.
Note also that the .sendto and .write methods already behave consistently and raise SSLWantWrite. It seems it's really just the send() method that is the lone outlier.
The attached patch changes ssl.send to raise SSLWantWrite instead of returning zero. The full testsuite still runs fine. I'm a bit sceptical though, because the code looks as if send() was deliberately written to catch the SSLWantWrite exception and return zero instead.. Can anyone familiar with the code comment on this?
A little hg sleuthing (which I assume you did but I'll record for the record) reveals that this was introduced by Bill Jansen in changeset 8a281bfc058d. Following the bugs mentioned in the checkin message, it looks like it *might* have been related to bpo-1251, but there really isn't enough information in the issues or the checkin to tell for sure. It certainly sounds like the problems mentioned in that issue may be relevant, though (the disconnection between the unecrypted data send and what actually gets placed on the wire and when).
I see you already added Bill Jansen to nosy, so that's probably the best bet for getting an answer, if we are lucky and he both responds and remembers :)
It's probably too late to change this, unfortunately. There are non-blocking frameworks and libraries out there relying on the current behaviour.
As for sendall(), it doesn't really make sense on a non-blocking socket anyway.
nikratio commented on Mar 16, 2014
nikratiomannequin
MannequinAuthor
More actions
Antoine, do you know that there are frameworks out there using this, or is that a guess? asyncio, for example, seems to expect an SSLWantWrite exception as well. (it also works with a zero return, but it's not clear from the code if that's by design or by a chance).
Antoine, do you know that there are frameworks out there using this,
or is that a guess?
It's just a guess.
nikratio commented on Mar 16, 2014
nikratiomannequin
MannequinAuthor
More actions
Twisted does not seem to rely on it either (there's no mention of SSLWant* in the source at all, and without that, you can't possibly have support for non-blocking ssl sockets).
nikratio commented on Mar 16, 2014
nikratiomannequin
MannequinAuthor
More actions
gevent is calling _sslobject.write() directly, so it would not be affected by any change.
nikratio commented on Mar 16, 2014
nikratiomannequin
MannequinAuthor
More actions
Tornado uses SSLSocket.send(), and it looks as if a SSLWantWrite exception is not caught but would propagate, so this would probably break.
nikratio commented on Mar 17, 2014
nikratiomannequin
MannequinAuthor
More actions
More info on twisted: it uses PyOpenSSL rather than the stdlib ssl module, so it's not affected at all.
nikratio commented on Mar 19, 2014
nikratiomannequin
MannequinAuthor
More actions
Since this behavior cannot be changed without breaking third-party libraries (why did they work around this rather than reporting a bug?), I'd suggest to document the current behavior and allow programs to opt-in to getting exceptions.
I've attached a patch to that end. Feedback would be appreciated.
Another option may be to have SSLSocket.send() convert the WANT_WRITE exception into a socket.error with errno EAGAIN. This wouldn't break Tornado and would make socket.send and SSLSocket.send more consistent, but it's weird to hide the true error like this.
I think that would only make sense if the SSLWant{Read/Write}Error
exceptions are eliminated completely, so that all methods raise
BlockingError (==EAGAIN) instead.
Raising BlockingError is marginally better than returning zero, but I
think not worth the change.
I vote -1 to adding a new flag to control whether it returns zero or
raises and +0 to just fixing it in Python 3.5 (I don't think returning
zero is an unreasonable thing to do; it's not obvious to me from
send(2) that it is guaranteed to never return zero although I believe
that to be the case). It'll break Tornado, but there will be plenty
of time to get a fix out before then.
If that's your opinion then I'm inclined to trust you.
Another option may be to have SSLSocket.send() convert the WANT_WRITE
exception into a socket.error with errno EAGAIN.
I don't think it's a good idea, since it hides the true reason of the
error (also, it suppresses the distinction between WANT_READ and
WANT_WRITE, which tells you whether you need to select() the socket for
reading or writing).
nikratio commented on Mar 27, 2014
nikratiomannequin
MannequinAuthor
More actions
As an alternative, I have attached a pure docpatch that just documents the future behavior.
Someone with commit privileges: please take your pick :-).
nikratio commented on Apr 27, 2014
nikratiomannequin
MannequinAuthor
More actions
As discussed on python-dev, here is a patch that changes the behavior of send() and sendall() to raise SSLWant* exceptions instead of returning zero.
python-dev commented on Apr 29, 2014
python-devmannequin
Mannequin
More actions
New changeset 3cf067049211 by Antoine Pitrou in branch 'default':
Issue bpo-20951: SSLSocket.send() now raises either SSLWantReadError or SSLWantWriteError on a non-blocking socket if the operation would block. Previously, it would return 0. http://hg.python.org/cpython/rev/3cf067049211
I can reproduce the failure here.
There might be different behaviour accross OpenSSL versions (mine is
1.0.1e).
nikratio commented on May 1, 2014
nikratiomannequin
MannequinAuthor
More actions
Maybe. I have 1.0.1g. Could you maybe post the output of the failed test? I'd like to understand how the patch broke the test (looking at your patch alone didn't tell me much).
Actually, the test hangs after one of the threads crashes:
test__all__ (test.test_poplib.TestPOP3_SSLClass) ... Exception in thread Thread-23:
Traceback (most recent call last):
File "/home/antoine/cpython/default/Lib/threading.py", line 920, in _bootstrap_innerself.run()
File "/home/antoine/cpython/default/Lib/test/test_poplib.py", line 218, in run
asyncore.loop(timeout=0.1, count=1)
File "/home/antoine/cpython/default/Lib/asyncore.py", line 212, in loop
poll_fun(timeout, map)
File "/home/antoine/cpython/default/Lib/asyncore.py", line 153, in poll
read(obj)
File "/home/antoine/cpython/default/Lib/asyncore.py", line 87, in read
obj.handle_error()
File "/home/antoine/cpython/default/Lib/asyncore.py", line 83, in read
obj.handle_read_event()
File "/home/antoine/cpython/default/Lib/asyncore.py", line 422, in handle_read_eventself.handle_accept()
File "/home/antoine/cpython/default/Lib/asyncore.py", line 499, in handle_acceptself.handle_accepted(*pair)
File "/home/antoine/cpython/default/Lib/test/test_poplib.py", line 228, in handle_acceptedself.handler_instance =self.handler(conn)
File "/home/antoine/cpython/default/Lib/test/test_poplib.py", line 368, in __init__self.push('+OK dummy pop3 server ready. <timestamp>')
File "/home/antoine/cpython/default/Lib/test/test_poplib.py", line 82, in push
asynchat.async_chat.push(self, data.encode("ISO-8859-1") +b'\r\n')
File "/home/antoine/cpython/default/Lib/asynchat.py", line 190, in pushself.initiate_send()
File "/home/antoine/cpython/default/Lib/asynchat.py", line 243, in initiate_sendself.handle_error()
File "/home/antoine/cpython/default/Lib/asynchat.py", line 241, in initiate_send
num_sent =self.send(data)
File "/home/antoine/cpython/default/Lib/asyncore.py", line 366, in send
result =self.socket.send(data)
File "/home/antoine/cpython/default/Lib/ssl.py", line 667, in sendreturnself._sslobj.write(data)
ssl.SSLWantReadError: The operation did not complete (read) (_ssl.c:1636)
This was due to a simplistic handling of asyncore SSL connections in test_poplib, which I've fixed by reusing the code from test_ftplib.
Activity
nikratio commentedon Mar 16, 2014
When using non-blocking operation, the SSLSocket.send method returns 0 if no data can be sent at this point.
This is counterintuitive, because in the same situation (write to non-blocking socket that isn't ready for IO):
This behavior also makes it more complicated to write code that works with both SSLSockets and regular sockets.
Since the current behavior undocumented at best (and contradicting the documentation at worst), can we change this in Python 3.5?
nikratio commentedon Mar 16, 2014
This is actually seems to be not just an inconvience, but a real bug: since SSLSocket.sendall() uses SSLSocket.send() internally, the former method will busy-loop when called on a non-blocking socket.
Note also that the .sendto and .write methods already behave consistently and raise SSLWantWrite. It seems it's really just the send() method that is the lone outlier.
The attached patch changes ssl.send to raise SSLWantWrite instead of returning zero. The full testsuite still runs fine. I'm a bit sceptical though, because the code looks as if send() was deliberately written to catch the SSLWantWrite exception and return zero instead.. Can anyone familiar with the code comment on this?
bitdancer commentedon Mar 16, 2014
A little hg sleuthing (which I assume you did but I'll record for the record) reveals that this was introduced by Bill Jansen in changeset 8a281bfc058d. Following the bugs mentioned in the checkin message, it looks like it *might* have been related to bpo-1251, but there really isn't enough information in the issues or the checkin to tell for sure. It certainly sounds like the problems mentioned in that issue may be relevant, though (the disconnection between the unecrypted data send and what actually gets placed on the wire and when).
I see you already added Bill Jansen to nosy, so that's probably the best bet for getting an answer, if we are lucky and he both responds and remembers :)
pitrou commentedon Mar 16, 2014
It's probably too late to change this, unfortunately. There are non-blocking frameworks and libraries out there relying on the current behaviour.
As for sendall(), it doesn't really make sense on a non-blocking socket anyway.
nikratio commentedon Mar 16, 2014
Antoine, do you know that there are frameworks out there using this, or is that a guess? asyncio, for example, seems to expect an SSLWantWrite exception as well. (it also works with a zero return, but it's not clear from the code if that's by design or by a chance).
pitrou commentedon Mar 16, 2014
It's just a guess.
nikratio commentedon Mar 16, 2014
Twisted does not seem to rely on it either (there's no mention of SSLWant* in the source at all, and without that, you can't possibly have support for non-blocking ssl sockets).
nikratio commentedon Mar 16, 2014
gevent is calling _sslobject.write() directly, so it would not be affected by any change.
nikratio commentedon Mar 16, 2014
Tornado uses SSLSocket.send(), and it looks as if a SSLWantWrite exception is not caught but would propagate, so this would probably break.
nikratio commentedon Mar 17, 2014
More info on twisted: it uses PyOpenSSL rather than the stdlib ssl module, so it's not affected at all.
nikratio commentedon Mar 19, 2014
Since this behavior cannot be changed without breaking third-party libraries (why did they work around this rather than reporting a bug?), I'd suggest to document the current behavior and allow programs to opt-in to getting exceptions.
I've attached a patch to that end. Feedback would be appreciated.
7 remaining items
nikratio commentedon Mar 26, 2014
On 03/25/2014 06:53 PM, Ben Darnell wrote:
I think that would only make sense if the SSLWant{Read/Write}Error
exceptions are eliminated completely, so that all methods raise
BlockingError (==EAGAIN) instead.
Raising BlockingError is marginally better than returning zero, but I
think not worth the change.
pitrou commentedon Mar 26, 2014
If that's your opinion then I'm inclined to trust you.
I don't think it's a good idea, since it hides the true reason of the
error (also, it suppresses the distinction between WANT_READ and
WANT_WRITE, which tells you whether you need to select() the socket for
reading or writing).
nikratio commentedon Mar 27, 2014
As an alternative, I have attached a pure docpatch that just documents the future behavior.
Someone with commit privileges: please take your pick :-).
nikratio commentedon Apr 27, 2014
As discussed on python-dev, here is a patch that changes the behavior of send() and sendall() to raise SSLWant* exceptions instead of returning zero.
python-dev commentedon Apr 29, 2014
New changeset 3cf067049211 by Antoine Pitrou in branch 'default':
Issue bpo-20951: SSLSocket.send() now raises either SSLWantReadError or SSLWantWriteError on a non-blocking socket if the operation would block. Previously, it would return 0.
http://hg.python.org/cpython/rev/3cf067049211
python-dev commentedon Apr 29, 2014
New changeset b0f6983d63df by Antoine Pitrou in branch 'default':
Add porting note for issue bpo-20951.
http://hg.python.org/cpython/rev/b0f6983d63df
pitrou commentedon Apr 29, 2014
Patch finally committed. Thanks Nikolaus!
python-dev commentedon Apr 29, 2014
New changeset 7f50e1836ddb by Antoine Pitrou in branch 'default':
Fix failure in test_poplib after issue bpo-20951.
http://hg.python.org/cpython/rev/7f50e1836ddb
pitrou commentedon Apr 29, 2014
Ok, there was a failure in test_poplib when run with -unetwork, I fixed it.
nikratio commentedon Apr 30, 2014
Antoine, are you sure this was a problem related to this patch?
The test seems to work just fine for me:
Am I doing something wrong?
pitrou commentedon Apr 30, 2014
I can reproduce the failure here.
There might be different behaviour accross OpenSSL versions (mine is
1.0.1e).
nikratio commentedon May 1, 2014
Maybe. I have 1.0.1g. Could you maybe post the output of the failed test? I'd like to understand how the patch broke the test (looking at your patch alone didn't tell me much).
pitrou commentedon May 1, 2014
Actually, the test hangs after one of the threads crashes:
This was due to a simplistic handling of asyncore SSL connections in test_poplib, which I've fixed by reusing the code from test_ftplib.