-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Handle overflow properly in core::slice #25300
Conversation
core::slice was originally written to tolerate overflow (notably, with slices of zero-sized elements), but it was never updated to use wrapping arithmetic when overflow traps were added. Also correctly handle the case of calling .nth() on an Iter with a zero-sized element type. The iterator was assuming that the pointer value of the returned reference was meaningful, but that's not true for zero-sized elements. Fixes rust-lang#25016.
r? @aturon (rust_highfive has picked a reviewer for you, use r? to override) |
@@ -684,13 +666,11 @@ macro_rules! iterator { | |||
fn next(&mut self) -> Option<$elem> { | |||
// could be implemented with slices, but this avoids bounds checks | |||
unsafe { | |||
::intrinsics::assume(!self.ptr.is_null()); | |||
::intrinsics::assume(!self.end.is_null()); |
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'm not sure why these assumes were added in the first place, they showed up in f9ef8cd without anything relevant in the commit message, but they aren't valid.
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.
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.
Why are they invalid?
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 don't have everything in fresh memory, but if these are invalid, we are in big trouble anyway (can't use .offset). Split in cases based on the size of T here too?
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.
They're potentially invalid if the element size is zero, because the iterators are free to wrap around in that case. Slices of zero-sized elements don't really have a meaningful pointer, but the iterators behave as though the element has a size of 1 in order to count the number of yielded elements correctly. This means that a slice with a sufficiently high pointer value and sufficiently large length will result in an iterator that wraps around.
I also question the edge case of a slice of non-zero-sized elements that includes the highest byte in the address space. It should be theoretically possible to have a valid slice that includes an element placed in the absolute highest address, and if so, then the end
pointer will have wrapped around to 0
and the ptr
value will wrap around when the iterator yields its final element. Although in this particular edge case I don't know if the use of offset()
is valid.
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 just added the assumptions back in a slightly different form. I believe the new version should be valid in all cases.
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'm of the impression that such an object size and placement would be illegal/impossible. If it isn't, we should make sure it is illegal in safe rustc. We can't safely index/address all bytes of that object otherwise.
GEP is our .offset()
.
If the GEP has the inbounds keyword, the result value is undefined (a “trap value”) if the GEP overflows (i.e. wraps around the end of the address space).
http://llvm.org/docs/GetElementPtr.html#what-happens-if-a-gep-computation-overflows
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.
@bluss Ok, I wasn't sure what actually happened if it overflowed. I assumed that the resulting value couldn't actually be dereferenced, but I thought it was plausible that it could still be used as a pointer comparison (since one-past-the-end is a valid location, and having an object occupy the last byte in the address space seems like a plausible thing to support). But if the documentation explicitly says it's a poison value then I guess it is.
That said, this only applies to slices of non-zero-sized elements. offset()
on a slice of zero-sized elements results in the same pointer (because anything multiplied by 0
is still 0
). The iterators use that macro specifically to get around this case, but e.g. indexing into a slice of zero-sized elements uses offset()
and that's fine (it just results in the same pointer that it was called on).
In light of this, I can re-add the end
assumption to next()
.
The previous assumptions were not valid for slices of zero-sized elements.
9ff95a6
to
e1e34e9
Compare
unsafe { | ||
if mem::size_of::<T>() != 0 { | ||
::intrinsics::assume(!self.ptr.is_null()); | ||
::intrinsics::assume(!self.end.is_null()); |
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'm assuming here that there's no need to make these assertions in the case where self.ptr == self.end
. We don't care about the pointer value in that case.
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.
Optimizing compilers are weird and chaotic. This will simply be revisited until it benches/codegens perfectly (it's the most important iterator after all).
@kballard would you be willing to confirm that none of the benchmarks that the assertions were introduced to add have changed? |
@gankro If you can tell me which benchmarks were expected to be affected by the assertions then I can run them locally. But the commit that introduced them didn't make any mention of the assertions so I wasn't sure what the actual effect was. |
@gankro Looking at #22466 (the PR that introduced these assumptions) Kimundi said:
So it's not clear that there even were any benchmarks affected by the addition of the assumptions in the first place. |
Good enough for me (still looking over the nitty gritty details). 🌴 |
Looking at #21886 actually reveals another issue: |
core::slice::Iter.ptr can be null when iterating a slice of zero-sized elements, but the pointer value used for the slice itself cannot. Handle this case by always returning a dummy pointer for slices of zero-sized elements.
I've added a commit to handle that case as well. I haven't actually run the tests locally for it yet, I'm going to let Travis CI do that for me. |
r=me once tests pass. Don't care a lot about my nits. |
Hmm, did Travis CI only run |
Nah it only does tidy these days |
I'll run a |
@bors r+ what are automation systems for but to test our junk for us? |
📌 Commit f2614f5 has been approved by |
core::slice was originally written to tolerate overflow (notably, with slices of zero-sized elements), but it was never updated to use wrapping arithmetic when overflow traps were added. Also correctly handle the case of calling .nth() on an Iter with a zero-sized element type. The iterator was assuming that the pointer value of the returned reference was meaningful, but that's not true for zero-sized elements. Fixes #25016.
core::slice was originally written to tolerate overflow (notably, with
slices of zero-sized elements), but it was never updated to use wrapping
arithmetic when overflow traps were added.
Also correctly handle the case of calling .nth() on an Iter with a
zero-sized element type. The iterator was assuming that the pointer
value of the returned reference was meaningful, but that's not true for
zero-sized elements.
Fixes #25016.