Skip to content
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

Properly handle io.EOF error conditions when reading #554

Merged
merged 1 commit into from Aug 12, 2023

Conversation

dsnet
Copy link
Contributor

@dsnet dsnet commented Jul 21, 2023

Previously, the Server.Serve method would never return nil, because the infinite for-loop handling request packets would only break if reading a packet reported an error.
A common termination condition is when the underlying connection is closed and recvPacket returns io.EOF.
In which case Serve should ignore io.EOF and
treat it as a normal shutdown.

However, this means that recvPacket must correctly handle io.EOF such that it never reports io.EOF if a packet is partially read. There are two calls to io.ReadFull in recvPacket.
The first call correctly forwards an io.EOF error
if no additional bytes of the next packet are read. However, the second call incorrectly forwards io.EOF when no bytes of the payload could be read.
This is incorrect since we already read the length and should convert the io.EOF into an io.ErrUnexpectedEOF.

dsnet added a commit to tailscale/tailscale that referenced this pull request Jul 21, 2023
If the connection provided to sftp.NewServer is closed,
Serve returns the io.EOF error verbatim from io.Reader.Read.
This is an odd error since this is an expected situation,
so we manually ignore io.EOF.
This is somewhat buggy since the sftp package itself
incorrectly reports io.EOF in cases where it should actually
be reporting io.ErrUnexpectedEOF.
See pkg/sftp#554 which patches Serve to
return nil on clean closes and fixes buggy uses of io.ReadFull.

Fixes #8592

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
bradfitz pushed a commit to tailscale/tailscale that referenced this pull request Jul 21, 2023
If the connection provided to sftp.NewServer is closed,
Serve returns the io.EOF error verbatim from io.Reader.Read.
This is an odd error since this is an expected situation,
so we manually ignore io.EOF.
This is somewhat buggy since the sftp package itself
incorrectly reports io.EOF in cases where it should actually
be reporting io.ErrUnexpectedEOF.
See pkg/sftp#554 which patches Serve to
return nil on clean closes and fixes buggy uses of io.ReadFull.

Fixes #8592

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
bradfitz pushed a commit to tailscale/tailscale that referenced this pull request Jul 21, 2023
If the connection provided to sftp.NewServer is closed,
Serve returns the io.EOF error verbatim from io.Reader.Read.
This is an odd error since this is an expected situation,
so we manually ignore io.EOF.
This is somewhat buggy since the sftp package itself
incorrectly reports io.EOF in cases where it should actually
be reporting io.ErrUnexpectedEOF.
See pkg/sftp#554 which patches Serve to
return nil on clean closes and fixes buggy uses of io.ReadFull.

Fixes #8592

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
(cherry picked from commit bb4b35e)
DentonGentry pushed a commit to tailscale/tailscale that referenced this pull request Jul 22, 2023
If the connection provided to sftp.NewServer is closed,
Serve returns the io.EOF error verbatim from io.Reader.Read.
This is an odd error since this is an expected situation,
so we manually ignore io.EOF.
This is somewhat buggy since the sftp package itself
incorrectly reports io.EOF in cases where it should actually
be reporting io.ErrUnexpectedEOF.
See pkg/sftp#554 which patches Serve to
return nil on clean closes and fixes buggy uses of io.ReadFull.

Fixes #8592

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
(cherry picked from commit bb4b35e)
Copy link
Collaborator

@puellanivis puellanivis left a comment

Choose a reason for hiding this comment

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

While looking for other cases of code that might also be affected the same way: Hm… we’re not bubbling up the error in https://github.com/pkg/sftp/blob/v1.13.5/request-server.go#L175-L177 RequestServer.Serve … and actually, it looks like there’s a “bug” in that if packetWorker ever returns err == nil we would close the connection, even though we should. (Considering there would no longer be any goroutine watching the connection!)

Would you mind wrapping that into this PR as well?

packet.go Show resolved Hide resolved
server.go Show resolved Hide resolved
Previously, the Server.Serve method would never return nil,
because the infinite for-loop handling request packets would
only break if reading a packet reported an error.
A common termination condition is when the underlying connection
is closed and recvPacket returns io.EOF.
In which case Serve should ignore io.EOF and
treat it as a normal shutdown.

However, this means that recvPacket must correctly handle io.EOF
such that it never reports io.EOF if a packet is partially read.
There are two calls to io.ReadFull in recvPacket.
The first call correctly forwards an io.EOF error
if no additional bytes of the next packet are read.
However, the second call incorrectly forwards io.EOF
when no bytes of the payload could be read.
This is incorrect since we already read the length and
should convert the io.EOF into an io.ErrUnexpectedEOF.
@dsnet
Copy link
Contributor Author

dsnet commented Aug 9, 2023

Thanks for the review.

While looking for other cases of code that might also be affected the same way: Hm… we’re not bubbling up the error in https://github.com/pkg/sftp/blob/v1.13.5/request-server.go#L175-L177 RequestServer.Serve … and actually, it looks like there’s a “bug” in that if packetWorker ever returns err == nil we would close the connection, even though we should. (Considering there would no longer be any goroutine watching the connection!)

I'm not sure I follow. Right now, packetWorker only ever returns nil, so the err != nil check in RequestServer.Serve never gets executed. Are you saying that we should always call rs.conn.Close() regardless of the error condition?

The code you pointed out certainly seems fishy, but I think it's sufficiently different that it might belong in a different PR.

@puellanivis
Copy link
Collaborator

😰 Ack, you’re right, it does only ever return nil. (I was after all checking on my phone on holiday, so my brain wasn’t entirely on.)

I think we should remove the check, and always call rs.conn.Close() but it looks like this is called in https://github.com/pkg/sftp/blob/master/request-server.go#L150 which defers a close of the channel that happens before the range on that channel in packetWorker, meaning that rs.conn.Close() should never need to execute in the first place. 😩 Except that rs.conn.Close() is only ever called if there is an error in the makePacket function, not in the actual reading… so, I’m not sure either of them ever actually execute. 😔

I think it might need quite a bit of logic examination and correction, which as you say, might be better in a separate PR. I’m just a little wary of letting it stand there in this likely broken state… 🤔

@puellanivis
Copy link
Collaborator

OK, let me know if you want to pick up the RequestServer changes, but since it looks like the behavior won’t change despite the changes you’re making, if you don’t want to pick them up, I’ll just go ahead and merge anyways.

@dsnet
Copy link
Contributor Author

dsnet commented Aug 11, 2023

I'd rather keep the PR targetted to just io.ReadFull usages, if that's alright with you.

@puellanivis puellanivis merged commit 669003c into pkg:master Aug 12, 2023
4 checks passed
alexelisenko pushed a commit to Control-D-Inc/tailscale that referenced this pull request Feb 15, 2024
If the connection provided to sftp.NewServer is closed,
Serve returns the io.EOF error verbatim from io.Reader.Read.
This is an odd error since this is an expected situation,
so we manually ignore io.EOF.
This is somewhat buggy since the sftp package itself
incorrectly reports io.EOF in cases where it should actually
be reporting io.ErrUnexpectedEOF.
See pkg/sftp#554 which patches Serve to
return nil on clean closes and fixes buggy uses of io.ReadFull.

Fixes tailscale#8592

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
Signed-off-by: Alex Paguis <alex@windscribe.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants