-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Add a buffer reader helper class that can be used to read buffers safely. #3471
Add a buffer reader helper class that can be used to read buffers safely. #3471
Conversation
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.
Minor API nit; however, otherwise, seems reasonable.
src/lib/support/BufferReader.h
Outdated
* enough octets available. | ||
*/ | ||
CHECK_RETURN_VALUE | ||
CHIP_ERROR ReadOctet(uint8_t * octet) { return Read(octet); } |
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.
Why not Read8
? This seems to be an odd API nomenclature exception.
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 catch. This started out as a different class with more-semantic function names, and I forgot to rename this one. Will fix.
src/lib/support/BufferReader.h
Outdated
* | ||
* Simple reader for reading little-endian things out of buffers. | ||
*/ | ||
class LittleEndianReader |
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'd recommend following both nlio and CHIPEncoding convention and create both LittleEndian and BigEndian namespaces with a reader class instance in each.
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.
Also good catch. I'm going to do LittleEndian::Reader
and not worry about the BigEndian version for now. If we ever need that (which I doubt), we can add it easily.
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.
So the issue here is that once I have chip::LittleEndian::Reader
and chip::Encoding::LittleEndian::*
, LittleEndian
becomes ambiguous and more full qualification is needed. I guess I can do chip::Encoding::LittleEndian::Reader
... @gerickson any preferences?
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 like chip::Encoding::LittleEndian::Reader
.
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 guess that ambiguity was because I was in code that had using namespace chip::Encoding
.
Size increase report for "nrfconnect-example-build" from 3b333f0
Full report output
|
Size increase report for "esp32-example-build" from 3b333f0
Full report output
|
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.
man, this stuff is crying out for a sscanf()-like facility
2035a93
to
ef473be
Compare
Size increase report for "nrfconnect-example-build" from 7257d44
Full report output
|
Size increase report for "esp32-example-build" from 7257d44
Full report output
|
…ely. The idea is to not have people doing ad-hoc, and often incorrect when they forget to update lengths, length checks. I did measure the codesize with the Read methods out-of-line, and it's larger than with the inline methods as far as I can tell. It might be worth re-measuring once we have more callers to see what things look like then.
ef473be
to
f63ccce
Compare
Size increase report for "nrfconnect-example-build" from 2ea6342
Full report output
|
|
||
static constexpr size_t data_size = sizeof(T); | ||
|
||
if (mAvailable < data_size) |
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.
thought: should we somehow try to make errors persistent? If I have (wrong really) code like:
Reader reader(buffer, 2);
reader.Read32(&foo);
return reader.Read16(&bar);
we do not notice that a value failed to be read in between. I imagine we generally want to either use some GetLength() call to figure out available length (if data is variable size) or once reader failed to read, it became invalid.
If we have persistent errors, we can have builder patters:
Reader reader(buffer, len);
return reader.Read8(&foo).Read16(&bar).Read8(&baz).StatusCode();
instead of a more verbose VerifyOrExit:
CHIP_ERROR err = CHIP_NO_ERROR;
Reader reader(buffer, len);
err = reader.Read8(&foo);
SuccessOrExit(err);
err = reader.Read16(&bar);
SuccessOrExit(err);
err = reader.Read8(&baz);
SuccessOrExit(err);
exit:
return err;
I could have similar SuccessOrExit(reader.StatusCode())
if I wanted to for some performance benefits, however I generally expect happy path to almost alwyas happen and would be willing to trade some ifs cost on failure for smaller flash usage.
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 I have (wrong really) code like
That code won't compile, because Read32
is marked as CHECK_RETURN_VALUE
and in this code that return value is being ignored.
You could have code like this, though:
CHIP_RESULT res;
Reader reader(buffer, 2);
res = reader.Read32(&foo);
return reader.Read16(&bar);
but hopefully at that point you would notice that you are not doing anything with res
. And even better would be if we could make the compiler notice...
If we have persistent errors, we can have builder patterns:
That does make sense, in cases when all the reads are unconditional. In cases when the reads are conditional on the value a previous read returned, people would need to make sure to examine the success value of the previous read. I guess we could maybe do that if we require that the reference we return will be used (for either an immediate unconditional read call or for an error check)?
One other note: I plan to build a data-model-specific reader on top of this one to address #2168. But I suspect that might also be possible with the reader pattern. Will check.
I'll prototype out this suggestion and see how the code and the resulting codesize look.
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.
@andy31415 OK, I put up some example code at https://github.com/bzbarsky-apple/connectedhomeip/tree/little-endian-reader-builder and https://github.com/bzbarsky-apple/connectedhomeip/tree/data-model-message-reader-builder for the builder pattern approach. It seems to use more codesize than the code I had before, though maybe it would do a bit better with some more out-of-lining, since Read
ends up with a bit more logic.
Fundamentally, the builder setup still ends up doing the "did the previous call succeed" on every read check, which is what the SuccessOrExit
was doing...
I did test instead setting mAvailable
to 0 on read failure, so we don't have the extra branch on the status, but the end result is the same in terms of codesize; presumably the assignment is about the same size as the status-check branch.
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 can iterate. I would expect it to be smaller in size, but only if used a lot (so that the save of no extra if is worth it to extra logic inside reads).
I am ok with the PR as is.
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, that works. I'm pretty happy to revisit once we have more consumers and can get better size data. Converting should not be that big a problem, I suspect....
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 did some more experimenting, and I think you're right and the builder pattern might be a better fit here. Filed #3495 and will put up a PR soon.
Size increase report for "esp32-example-build" from 2ea6342
Full report output
|
The idea is to not have people doing ad-hoc, and often incorrect when
they forget to update lengths, length checks.
I did measure the codesize with the Read methods out-of-line, and it's
larger than with the inline methods as far as I can tell. It might be
worth re-measuring once we have more callers to see what things look
like then.
Problem
It's too easy to forget to update available length when reading things out of buffers and hence end up with potential buffer overruns.
Summary of Changes
Add a class that encapsulates the updating of the read pointer and the updating of the available length in one place in a way that guarantees they stay in sync.
Fixes #2914
One open question: Do we still want the
VerifyOrExit
calls that verify thatoctets_read
matches some value? It's not clear that we do: they used to be there to ensure the number we were about to cast touint16_t
would in fact fit inuint16_t
, but that's no longer in question... Is there another real use for those checks?