-
Notifications
You must be signed in to change notification settings - Fork 726
stream: avoid swallowing unexpected eof condition. #1299
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
Conversation
Codecov Report
@@ Coverage Diff @@
## main #1299 +/- ##
==========================================
+ Coverage 95.55% 95.78% +0.23%
==========================================
Files 60 60
Lines 14351 14339 -12
==========================================
+ Hits 13713 13735 +22
+ Misses 638 604 -34
... and 1 file with indirect coverage changes 📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
This commit introduces a unit tests that verify that a `Stream` or `StreamOwned` that receives an EOF from a peer that hasn't signalled the intent to close should read an `UnexpectedEOF` error. Additionally, since there was no existing coverage for `read_buf` this is added along with some small helpers.
Previously the `Stream` and `OwnedStream` types `read` and `read_buf` functions had code to try and "prematurely signal EOF" by returning `Ok(0)` if no bytes were read from the underlying transport. In practice this has the effect of swallowing an error condition when the remote peer has closed their end of the connection without properly signalling their intent to do so. When this happens the client must read an `UnexpectedEof` error to prevent a truncation attack. With the pre-existing code this condition is instead presented as a normal EOF. There doesn't appear to be a reason to "prematurely" signal anything - if we read 0 bytes from `complete_io()` we should instead break our `wants_read()` loop to allow execution to continue to a `self.conn.reader().read(...)` or `self.conn.reader().read_buf(...)` call where the common connection code can properly handle this case, turning the result into the desired `UnexpectedEof` error. This resolves the problem of distinguishing unexpected EOF from expected EOF and does not appear to break any other functionality expected of the `Stream` types.
5eca6da
to
fbe7859
Compare
@djc OK if I merge this one? |
FYI I did a little additional git spelunking ( |
Description
Previously the
Stream
andOwnedStream
type'sread
andread_buf
functions had code to try and "prematurely signal EOF" by returningOk(0)
if no bytes were read from the underlying transport after callingcomplete_io
.In practice this has the effect of swallowing an error condition when the remote peer has closed their end of the connection without properly signalling their intent to do so. When this happens the client must read an
UnexpectedEof
error to prevent a truncation attack. Clients reading through aStream
type with the pre-existing code would always read a normal EOF, whereas using a connection directly without the stream abstraction would present the correct error.There doesn't appear to be a reason to "prematurely" signal anything - if we read 0 bytes from
complete_io()
we should instead break ourwants_read()
loop to allow execution to continue to aself.conn.reader().read(...)
orself.conn.reader().read_buf(...)
call where the common connection code can properly handle this case (using the logic added in #790), turning the result into the desiredUnexpectedEof
error. Similarly there's no need to callprocess_new_packets()
ourselves becausecomplete_io()
does this after reading TLS bytes.Making this change resolves the problem of distinguishing unexpected EOF from expected EOF when using
Stream
orOwnedStream
and doesn't appear to break any other functionality expected of theStream
types. I did some git spelunking and wasn't able to determine the original purpose of the premature signalling logic (It was added in c94c223 by @ctz.) but I suspect code churn elsewhere makes it no longer necessary. If I've missed an important detail I'm happy to revisit.Testing
To emphasize the bug fix I've separated out the unit test as a commit added prior to the fix.
Before applying the fix an Ok 0 byte result from a read is returned where we expect an error:
After applying the fix the tests begin to pass:
Resolves #1188, tokio-rs/tls#128, tokio-rs/tls#141