-
Notifications
You must be signed in to change notification settings - Fork 122
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 integer overflows and truncation #278
Conversation
If tempfile is used on a 32 bit system, usize is u32. This means that casting u64 to usize may truncate the value. Test added to clarify the issue.
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.
Overall, this makes the code much harder to read (except the first fix) for a correctness issue that's entirely theoretical (except in the first case). Did you run into any actual issues?
@@ -96,7 +96,7 @@ impl SpooledTempFile { | |||
} | |||
|
|||
pub fn set_len(&mut self, size: u64) -> Result<(), io::Error> { | |||
if size as usize > self.max_size { | |||
if size > self.max_size as u64 { |
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.
This one is definitely an improvement.
src/spooled.rs
Outdated
if cursor | ||
.position() | ||
.checked_add(buf.len() as u64) | ||
.filter(|&val| val <= self.max_size as u64) | ||
.is_none() |
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.
This one isn't necessary (would require more addressable memory than a usize
).
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.
You could seek up to usize::MAX before calling write, then it will overflow. I haven't added a test case for this, because it could lead to a flush.
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.
In fact, the test is easy. Added to commit for reference.
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.
Ah, I see. You can seek past the end of the file. Yeah, you're right. And that's definitely something that can happen in practice.
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.
Can we rewrite it with saturating add? I.e.: cursor.position.saturating_add(buf.len() as u64) > self.max_size as u64
.
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.
Yes, fortunately we can. I've had the false assumption that it would be an issue if self.max_size
itself is usize::MAX
but in this case, the underlying write
calls will fail anyway.
Thanks for feedback so these is_none
-checks can be removed from both places!
src/spooled.rs
Outdated
if bufs | ||
.iter() | ||
.try_fold(cursor.position(), |a, b| a.checked_add(b.len() as u64)) | ||
.filter(|&val| val <= self.max_size as u64) | ||
.is_none() |
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 this may be possible if someone tried to repeatedly write the same large slice over and over? But then they likely have other issues.
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, how about we write this entire check with saturating add? The filter + is_none is... going to trip me up eventually.
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.
Yes, disliked this way of writing it as well. Fortunately it can be simplified as suggested. Done.
src/util.rs
Outdated
let capacity = prefix | ||
.len() | ||
.checked_add(suffix.len()) | ||
.and_then(|value| value.checked_add(rand_len)) | ||
.ok_or(io::Error::new(io::ErrorKind::InvalidInput, "name too long"))?; |
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.
This is kind of pointless as the user would have to pass an insanely large filename (which would already cause issues).
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.
Technically, the rand_len just has to be large enough, which just requires a .rand_bytes(usize::MAX)
call. No need for a huge allocation of a filename beforehand.
But yes, you could argue that it's a user error to call that function with such a large value.
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.
(well, it'll likely run out of memory and panic if the length is anywhere near usize::MAX)
But... how about we just use saturating math here as well (this is just for capacity). It avoids the error case and, and avoids the and_then
etc.
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.
Yeah, why not? I wanted to avoid the panic, but if someone really wants to try to allocate THAT much or just more than available, standard Rust will run into a panic anyway. The user asked for trouble doing that. :)
Adjusted as well. And removed the test.
No, it's been about making the API robust against such values. I've split the commits into two, since I assumed that it might be argued this way. So... Fell free to cherry pick, otherwise I can drop the second one. |
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, you're right. We might as well fix these. But see my comments on using saturating add instead.
src/spooled.rs
Outdated
if cursor | ||
.position() | ||
.checked_add(buf.len() as u64) | ||
.filter(|&val| val <= self.max_size as u64) | ||
.is_none() |
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.
Ah, I see. You can seek past the end of the file. Yeah, you're right. And that's definitely something that can happen in practice.
src/spooled.rs
Outdated
if bufs | ||
.iter() | ||
.try_fold(cursor.position(), |a, b| a.checked_add(b.len() as u64)) | ||
.filter(|&val| val <= self.max_size as u64) | ||
.is_none() |
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, how about we write this entire check with saturating add? The filter + is_none is... going to trip me up eventually.
src/spooled.rs
Outdated
if cursor | ||
.position() | ||
.checked_add(buf.len() as u64) | ||
.filter(|&val| val <= self.max_size as u64) | ||
.is_none() |
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.
Can we rewrite it with saturating add? I.e.: cursor.position.saturating_add(buf.len() as u64) > self.max_size as u64
.
src/util.rs
Outdated
let capacity = prefix | ||
.len() | ||
.checked_add(suffix.len()) | ||
.and_then(|value| value.checked_add(rand_len)) | ||
.ok_or(io::Error::new(io::ErrorKind::InvalidInput, "name too long"))?; |
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.
(well, it'll likely run out of memory and panic if the length is anywhere near usize::MAX)
But... how about we just use saturating math here as well (this is just for capacity). It avoids the error case and, and avoids the and_then
etc.
Passing huge values might lead to integer overflows during calculations. Signed-off-by: Tobias Stoeckmann <[email protected]>
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.
Thanks for the fixes and for working with me to find a good solution.
Published as v3.10.1. |
Thank you as well for the feedback and especially for maintaining the crate! |
Make sure that calculations do not overflow.
Also do not truncate u64 to usize on 32 bit systems.
I have added tests to showcase the issues.