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 integer overflows and truncation #278

Merged
merged 2 commits into from
Feb 26, 2024
Merged

Conversation

stoeckmann
Copy link
Contributor

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.

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.
@stoeckmann stoeckmann changed the title Fix integer overflow and truncation Fix integer overflows and truncation Feb 26, 2024
Copy link
Owner

@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.

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 {
Copy link
Owner

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
Comment on lines 160 to 164
if cursor
.position()
.checked_add(buf.len() as u64)
.filter(|&val| val <= self.max_size as u64)
.is_none()
Copy link
Owner

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).

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Owner

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.

Copy link
Owner

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.

Copy link
Contributor Author

@stoeckmann stoeckmann Feb 26, 2024

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
Comment on lines 179 to 183
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()
Copy link
Owner

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.

Copy link
Owner

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.

Copy link
Contributor Author

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
Comment on lines 8 to 12
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"))?;
Copy link
Owner

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).

Copy link
Contributor Author

@stoeckmann stoeckmann Feb 26, 2024

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.

Copy link
Owner

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.

Copy link
Contributor Author

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.

@stoeckmann
Copy link
Contributor Author

Did you run into any actual issues?

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.

Copy link
Owner

@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.

Ok, you're right. We might as well fix these. But see my comments on using saturating add instead.

src/spooled.rs Outdated
Comment on lines 160 to 164
if cursor
.position()
.checked_add(buf.len() as u64)
.filter(|&val| val <= self.max_size as u64)
.is_none()
Copy link
Owner

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
Comment on lines 179 to 183
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()
Copy link
Owner

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
Comment on lines 160 to 164
if cursor
.position()
.checked_add(buf.len() as u64)
.filter(|&val| val <= self.max_size as u64)
.is_none()
Copy link
Owner

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
Comment on lines 8 to 12
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"))?;
Copy link
Owner

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]>
Copy link
Owner

@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.

Thanks for the fixes and for working with me to find a good solution.

@Stebalien Stebalien merged commit 56c5934 into Stebalien:master Feb 26, 2024
13 checks passed
@Stebalien
Copy link
Owner

Published as v3.10.1.

@stoeckmann
Copy link
Contributor Author

Thanks for the fixes and for working with me to find a good solution.

Thank you as well for the feedback and especially for maintaining the crate!

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