Skip to content

Avoid certain panics in decoding. #83

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open

Avoid certain panics in decoding. #83

wants to merge 4 commits into from

Conversation

partim
Copy link
Member

@partim partim commented Apr 11, 2025

This PR changes most of the decoding code to be mostly panic free.

Specifically, it removes the use of slice indexing as well as unwrap and expect on options and results.

It does not do this for encoding – which requires some redesign in the traits – and for the string types – which will be done in separate PRs to make reviewing of this PR easier.

The PR introduces a breaking change in the decode::Source trait by changing how invalid indexes are treated. The bytes method now returns an optional Bytes, returning None if the indexes are invalid. If the advance method is called with an invalid length, the source is expected to go into some error state. Finally, the error type of take_opt_u8 was changed to allow an explicit error.

This PR raises the minimum supported Rust version to 1.74.

Copy link

@bal-e bal-e left a comment

Choose a reason for hiding this comment

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

Looks good, @partim, but I'd suggest taking a slightly different approach to failing silently when unexpected conditions occur. I'd at least suggest sprinkling debug_assert!() all over the place -- it's the same tool one would use when writing unsafe code, which can fail worse than silently.

Comment on lines 389 to +394
if self.source.request(remaining)? < remaining {
Err(self.source.content_err("unexpected end of data"))
}
else {
Ok(&self.source.slice()[..remaining])
return Err(self.source.content_err("unexpected end of data"))
}
self.source.slice().get(..remaining).ok_or_else(|| {
self.source.content_err("unexpected end of data")
})
Copy link

Choose a reason for hiding this comment

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

At a cursory glance, the checked get makes the initial check redundant, right? You should be able to simply omit it -- if not enough data is retrieved, the get will fail.

self.source.limit().unwrap()
// The source is guaranteed to have a limit by the code creating
// the primitive, so we can just return 0 here if it isn’t. This
// isn’t ideal and we would rater guarantee things through types,
Copy link

Choose a reason for hiding this comment

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

typo: "rater" -> "rather"

pos: usize,

/// The offset for the reported position.
///
/// This is the value reported by `Source::pos` when `self.pos` is zero.
/// This is 0, unles the value was created with `Self::with_offset`.
Copy link

Choose a reason for hiding this comment

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

typo: "unles" -> "unless"

Comment on lines +435 to +437
data[i] = source.slice().get(i).copied().ok_or_else(|| {
source.content_err("source returned short slice")
})?;
Copy link

Choose a reason for hiding this comment

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

Given how much you need to check that the Source is working correctly, perhaps it would be worthwhile to change its API to be less fallible.

Copy link
Member Author

Choose a reason for hiding this comment

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

I have a plan to rewrite both Tag and Length in a follow-up PR to be more robust. Should have mentioned that in the PR comment but forgot.

Comment on lines -543 to +546
// Unwrapping here is okay. The only error that can happen is that
// the tag is longer that we support. However, we already checked that
// there’s only OctetString or End of Value tags which we _do_
// support.
// Taking the tag and length shouldn’t fail since we checked already,
// so just returning `None` should be fine.
Copy link

Choose a reason for hiding this comment

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

Does this mean that a bug here would lead to a silent/hidden failure rather than a panic? Maybe it would be good to leave a debug_assert!() or similar here, so that you have some avenue of detecting these bugs. That might apply to the whole PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

I am quite unhappy with all the string type handling, so I’ll probably do a rewrite here as well.

Comment on lines +1248 to +1265
assert_eq!(make([0]).as_slice(), b"\0");
assert_eq!(make([0, 0]).as_slice(), b"\0");

assert_eq!(make([0x10]).as_slice(), b"\x10");
assert_eq!(make([0, 0x10]).as_slice(), b"\x10");
assert_eq!(make([0, 0, 0x10]).as_slice(), b"\x10");

assert_eq!(make([0x10, 0xF0]).as_slice(), b"\x10\xF0");
assert_eq!(make([0, 0x10, 0xF0]).as_slice(), b"\x10\xF0");
assert_eq!(make([0, 0, 0x10, 0xF0]).as_slice(), b"\x10\xF0");

assert_eq!(make([0xF0]).as_slice(), b"\0\xF0");
assert_eq!(make([0, 0xF0]).as_slice(), b"\0\xF0");
assert_eq!(make([0, 0, 0xF0]).as_slice(), b"\0\xF0");

assert_eq!(make([0xF0, 0xF0]).as_slice(), b"\0\xF0\xF0");
assert_eq!(make([0, 0xF0, 0xF0]).as_slice(), b"\0\xF0\xF0");
assert_eq!(make([0, 0, 0xF0, 0xF0]).as_slice(), b"\0\xF0\xF0");
Copy link

Choose a reason for hiding this comment

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

It's probably a lot of work to support, but proptest would be a great way to fuzz the whole library.

Copy link
Member Author

Choose a reason for hiding this comment

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

There are a bunch of fuzzing tests already, currently implemented manually with cargo fuzz. I’ll have a look at proptest and see if that makes it easier to be more complete.

@partim
Copy link
Member Author

partim commented Apr 14, 2025

I am now considering changing the whole thing more drastically. I want to get rid of the built-in support for Bytes, so I think we can merge Source::reserve and Source::slice into one method that gives you exactly as many octets as you ask for or errors out. That would leave Source::advance which we could get rid of by having this new method not return a blank slice but a newtype that you then have to either consume or return an error. We’d have to try the latter to see if it actually works in practice.

But it would avoid the need for debug assertions.

@bal-e
Copy link

bal-e commented Apr 14, 2025

The change to Source sounds good. I'm curious to see how the newtype approach works out -- it can be quite tricky.

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