-
Notifications
You must be signed in to change notification settings - Fork 390
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
Conversation
When opening a PR that fixes an issue, please put |
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) { |
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.
That cast will not work out... rather cast them both to u64
for comparison.
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.
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?
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.
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
.
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.
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))
}
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.
Hm, that will be really confusing with the scalar to_usize
though.^^
Maybe try_into/from_host_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.
OK I'll add them :)
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 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))?; |
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... 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.
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 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...
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.
@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?
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.
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()
.
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, 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.
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.
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
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 we truncate the buffer size we pass to Read::read
we know the return value can't be bigger than that.
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.
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?
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 think I figured it out for read
. I'm not sure if write
is OK tho.
☔ The latest upstream changes (presumably #1044) made this pull request unmergeable. Please resolve the merge conflicts. |
I'm capping |
Thanks! @bors r+ |
📌 Commit 4baef71 has been approved by |
if count == 0 { | ||
return Ok(0); | ||
} |
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.
Note to self: try removing this, now that we don't force_ptr
any more.
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
☀️ Test successful - checks-travis, status-appveyor |
This PR takes care of two problems:
Memory::(read|write)_bytes
to guarantee that memory accesses are checked (Fixes: Some direct memory accesses lack bounds-checks #1007)(get|remove)_handle_and
methods which were a little bit cumbersome to use. In particularremove_handle_and
, because we were using it to avoid borrowing issues before theEvaluator::memory
field was public.@RalfJung @oli-obk