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

Fix read bug in readByteBuf #60

Merged
merged 3 commits into from
Feb 23, 2022

Conversation

MarcoPolo
Copy link
Contributor

@MarcoPolo MarcoPolo commented Feb 10, 2022

readByteBuf was incorrectly returning the error (EOF) before returning the first byte read. It is a valid for an io.Reader to return both n == 1 and err == EOF (see https://pkg.go.dev/io#Reader). But readByteBuf dropped the read byte and instead returned EOF.

This uses io.ReadFull to handle this logic, where io.ReadFull follows these semantics:

ReadFull reads exactly len(buf) bytes from r into buf. It returns the number of bytes copied and an error if fewer bytes were read. The error is EOF only if no bytes were read. If an EOF happens after reading some but not all the bytes, ReadFull returns ErrUnexpectedEOF. On return, n == len(buf) if and only if err == nil. If r returns an error having read at least len(buf) bytes, the error is dropped.

This also makes sure that the discard helper also follows those semantics. Returning EOF if no bytes were discarded and ErrUnexpectedEOF if only some of the bytes were discarded.


Note this started as a continuation of #44, but we realized along the way that most of this code was correct by the semantics above.

This change fixes two things:

  1. readyByteBuf bug
  2. discard not consistently returning io.EOF or io.ErrUnexpectedEOF

@MarcoPolo MarcoPolo mentioned this pull request Feb 10, 2022
Copy link
Collaborator

@Stebalien Stebalien left a comment

Choose a reason for hiding this comment

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

LGTM!

@Stebalien
Copy link
Collaborator

Note: there is a tiny problem here. Ideally we would return EOF if we read no bytes at all. But that's probably not worth it.

@MarcoPolo
Copy link
Contributor Author

Okay. Agreed. I’ll make that change later today.

@MarcoPolo
Copy link
Contributor Author

Updated and added a comment here https://github.com/whyrusleeping/cbor-gen/pull/60/files#diff-189b40dd496fee4378c7839f550f27e38db377a64fbe0881ce48651f0ac809faR64-R68

That explains the general logic of when things return EOF vs. ErrUnexpectedEOF.

@MarcoPolo
Copy link
Contributor Author

Ideally we would return EOF if we read no bytes at all.

I think this is correct, but then what's the point of ReadExact? io.ReadFull does this:

The error is EOF only if no bytes were read. If an EOF happens after reading some but not all the bytes, ReadFull returns ErrUnexpectedEOF.

@MarcoPolo
Copy link
Contributor Author

Continuing the thought above, I think most of the old code is correct. EOF happens when we read no bytes and ErrUnexpectedEOF when we read some but not all and ran into EOF.

So I'm going to discard most of the commits and fix only the readByte and readByteBuf, and check that discard follows the same semantics.

@MarcoPolo MarcoPolo marked this pull request as draft February 11, 2022 22:35
@Stebalien
Copy link
Collaborator

I think this is correct, but then what's the point of ReadExact? io.ReadFull does this:

Because we were using ReadExact internally. Basically:

  1. If I try to decode a CBOR object and read nothing, I should return an EOF.
  2. If I try to decode a CBOR object and read some bytes, I should return an UnexpectedEOF.

@MarcoPolo MarcoPolo changed the title Protect against stray EOFs #2 Fix read bug in readByteBuf Feb 11, 2022
@MarcoPolo MarcoPolo marked this pull request as ready for review February 11, 2022 23:20
@MarcoPolo
Copy link
Contributor Author

yep! I think we're on the same page. My point was that with that change ReadExact becomes ReadFull, or in other words the implementation of ReadExact becomes:

func ReadExact(r io.Reader, buf []byte) error {
	_, err := io.ReadFull(r, buf)
	return err
}

So then a lot of the changes in #44 aren't needed.

@MarcoPolo
Copy link
Contributor Author

Updated this PR and the description.

I took out most of the changes from #44 because they aren't needed since io.ReadFull already does this.

I fixed the bug in readByteBuf, and I made discard return the correct eof error kind.

For general reference, the comment on io.EOF also summarizes this distinction pretty well:

EOF is the error returned by Read when no more input is available. (Read must return EOF itself, not an error wrapping EOF, because callers will test for EOF using ==.) Functions should return EOF only to signal a graceful end of input. If the EOF occurs unexpectedly in a structured data stream, the appropriate error is either ErrUnexpectedEOF or some other error giving more detail.

Copy link
Collaborator

@Stebalien Stebalien left a comment

Choose a reason for hiding this comment

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

Strictly speaking, this code is correct. But it doesn't fix the original bug. We need to ensure that UnmarshalCBOR returns:

  1. io.EOF if it reads nothing.
  2. io.UnexpectedEOF if it reads some data but hits an EOF before it finishes the object.

As far as I can tell, the current code can still return an EOF if, e.g., stop reading a CBOR object on some field boundary.

@MarcoPolo
Copy link
Contributor Author

Yeah I think that's true. This does fix the other bug in readByteBuf.

@Stebalien / @whyrusleeping could either of you merge this? I don't have that ability

@Stebalien Stebalien merged commit ebcc1e8 into whyrusleeping:master Feb 23, 2022
@Stebalien
Copy link
Collaborator

Done.

masih added a commit to filecoin-project/lotus that referenced this pull request Mar 2, 2022
The dependency to cbor-gen includes a bug fix on how `readByteBuf`
handled errors. See:
- whyrusleeping/cbor-gen#60
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.

2 participants