Skip to content

Fix unchecked memory access for files #1022

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

Merged
merged 12 commits into from
Nov 13, 2019
Merged

Conversation

pvdrz
Copy link
Contributor

@pvdrz pvdrz commented Oct 24, 2019

This PR takes care of two problems:

  • It uses Memory::(read|write)_bytes to guarantee that memory accesses are checked (Fixes: Some direct memory accesses lack bounds-checks #1007)
  • It removes the (get|remove)_handle_and methods which were a little bit cumbersome to use. In particular remove_handle_and, because we were using it to avoid borrowing issues before the Evaluator::memory field was public.

@RalfJung @oli-obk

@pvdrz pvdrz changed the title Fix unchecked memory access for files. Fix unchecked memory access for files Oct 25, 2019
@RalfJung
Copy link
Member

When opening a PR that fixes an issue, please put Fixes: #xxx into the PR description so that the issue gets closed automatically when the PR gets merged.

src/shims/fs.rs Outdated

if let Ok(c) = result {
// Check that we read less than `i64::MAX` bytes.
if c > (i64::max_value() as usize) {
Copy link
Member

Choose a reason for hiding this comment

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

That cast will not work out... rather cast them both to u64 for comparison.

Copy link
Member

Choose a reason for hiding this comment

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

But I think this check is really not needed (as you said before I think); we have the call count many bytes which is already sufficiently bounded.

The weird part is that count is a usize but later you make it an i64... why that? What are the relevant types on the interpreted machine?

Copy link
Contributor Author

@pvdrz pvdrz Nov 3, 2019

Choose a reason for hiding this comment

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

the signature for read is:

ssize_t read(int fd, void *buf, size_t count);

So, the count argument is an usize but the return type is isize. I have no idea what should happen if someone passes a value larger than isize::max_value() as count.

Edit: the same is true for write.

Copy link
Member

Choose a reason for hiding this comment

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

These are target usize though. So e.g. on a 32bit host with a 64bit target, these cannot be loslessly converted to host usize.

Target usize is u64 in Miri. Ideally we'd use TryInto to convert that into usize, but unfortunately those instances do not exist. So instead I suggest adding helper methods like

fn try_into_usize(i: impl Into<u128>) -> Option<usize>
{
  let i: u128 = i;
  if i > usize::max_value() as u128 { None } else { Some(i as usize) }
}
fn try_from_usize<T: TryFrom<u128>>(i: usize) -> Option<T>
{
  T::try_from(u128::from(i))
}

Copy link
Member

Choose a reason for hiding this comment

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

Hm, that will be really confusing with the scalar to_usize though.^^

Maybe try_into/from_host_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.

OK I'll add them :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added the functions and I'm using them in read and write (see 56c5e53). I don't like how it looks but it works. Maybe we can reformulate try_unwrap_io_result somehow.

src/shims/fs.rs Outdated
match result {
Ok(c) => {
let read_bytes = helpers::try_from_host_usize::<i64>(c)
.ok_or_else(|| err_unsup_format!("Number of read bytes {} cannot be transformed to i64", c))?;
Copy link
Member

Choose a reason for hiding this comment

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

So... here we convert to i64 and then, later, we convert to what the target platform really needs. What will happen if that second part is the one that overflows? It's probably a from_int in foreign_items.rs, and that will ICE if the result is too big. So, the error handling isn't really adequate.

But also, do we need this? We know c <= count. What is the largest possible count? You used to_usize, is that right? It seems the type in C is size_t, which would be isize, not usize! So we should know that the buffer is definitely not too big to represent the result.

The only place where we can actually error is if the program does sth too big for the target, but that is already handled above.

Copy link
Member

Choose a reason for hiding this comment

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

Ah no, size_t is unsigned, I was wrong there. But that's silly, what does the original function do if the size fits into a size_t but not an ssize_t?!? I hate C...

Copy link
Member

Choose a reason for hiding this comment

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

@oli-obk any opinions? My conclusion is that the API we are trying to emulate is inherently broken and there is nothing reasonable we can do, so we should do something simple and consistent.^^

We could interpret the count parameter as signed, so if it is "too big" it'll be negative, and then we usize usize::try_from and if that complains we throw an "unsupported big buffer" or so?

Copy link
Contributor

Choose a reason for hiding this comment

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

Since the docs on read say

Upon successful completion, read() and pread() shall return a non-negative integer indicating the number of bytes actually read. Otherwise, the functions shall return -1 and set errno to indicate the error.

I would say that any value larger than isize::max_value() (for the target isize) should throw an interpreter error. If the target is 64 bit, it can't happen anyway, because we can't have vectors larger than isize::max_value().

Copy link
Member

@RalfJung RalfJung Nov 6, 2019

Choose a reason for hiding this comment

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

Well, these functions don't have to fill the entire buffer. So I assume what happens is, they truncate saturate the input to isize::MAX and will never use more than that.

Copy link
Contributor

Choose a reason for hiding this comment

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

right, so the usize we get from Rust's Read::read will never be > i64::max_value(), so for 64bit host platforms interpreting 32 bit target platforms, we basically have to give up when such a situation occurs, since it's too late to do anything about it. Everywhere else, it should be fine

Copy link
Member

Choose a reason for hiding this comment

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

If we truncate the buffer size we pass to Read::read we know the return value can't be bigger than that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me see if IIUC. So that basically means being sure that the count parameter doesn't exceeds isize::max_value() so that when we create the intermediate buffer for reading/writing, it isn't larger than isize::max_value() and we can return a proper value at the end. Sounds good to you?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I figured it out for read. I'm not sure if write is OK tho.

@bors
Copy link
Contributor

bors commented Nov 8, 2019

☔ The latest upstream changes (presumably #1044) made this pull request unmergeable. Please resolve the merge conflicts.

@pvdrz
Copy link
Contributor Author

pvdrz commented Nov 9, 2019

I'm capping count at the beginning of both functions and try_from(count).unwrap()ing everything.

@RalfJung
Copy link
Member

Thanks!

@bors r+

@bors
Copy link
Contributor

bors commented Nov 13, 2019

📌 Commit 4baef71 has been approved by RalfJung

Comment on lines 179 to 181
if count == 0 {
return Ok(0);
}
Copy link
Member

Choose a reason for hiding this comment

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

Note to self: try removing this, now that we don't force_ptr any more.

@bors
Copy link
Contributor

bors commented Nov 13, 2019

⌛ Testing commit 4baef71 with merge 09b0a8a...

bors added a commit that referenced this pull request Nov 13, 2019
Fix unchecked memory access for files

This PR takes care of two problems:

- It uses `Memory::(read|write)_bytes` to guarantee that memory accesses are checked (Fixes: #1007)
- It removes the `(get|remove)_handle_and` methods which were a little bit cumbersome to use. In particular `remove_handle_and`, because we were using it to avoid borrowing issues before the `Evaluator::memory` field was public.

@RalfJung @oli-obk
@bors
Copy link
Contributor

bors commented Nov 13, 2019

☀️ Test successful - checks-travis, status-appveyor
Approved by: RalfJung
Pushing 09b0a8a to master...

@bors bors merged commit 4baef71 into rust-lang:master Nov 13, 2019
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.

Some direct memory accesses lack bounds-checks
4 participants