-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
(Maybe) Undefined behavior in safe code from getelementptr inbounds
with offset 0
#54857
Comments
Pardon me, but does this actually results in miscompilation? I'm aware the |
cc @rust-lang/wg-unsafe-code-guidelines |
Thanks for bringing this up! This is indeed an interesting question. And it's the same question in both cases: What, if any, are the "inbounds" addresses for non-NULL integer pointers (i.e., pointer created from integers instead of an actual allocation)? I recall chatting with @arielb1 about this, and the answer I got (IIRC) was that all the integer pointers form one huge allocation excluding NULL which is in some logical way disjoint from the actual addresses covered by real allocations. So, my understanding is that This also matches the formal model that we recently proposed. That paper proves correctness of at least some of the alias analysis done by LLVM, showing that the model is compatible with what LLVM does. Basically, the reason this works is that the resulting pointer (in the "integer allocation") can never be dereferenced, so it doesn't really matter what alias analysis thinks it might alias with. It would be interesting to get an official answer from LLVM on this, and I wouldn't know that we can do much until we get clarification from their side. We could rule out |
So @RalfJung's model successfully performs "optimization chicken" for pointers that come from integers. What I said was the situation with LLVM was entirely different - it's more of an "LLVM-ABI interface" issue: why do you think there isn't a valid 0-sized object at address This is similar to being able to This is not an "optimization chicken" thing - if you start from an OTOH, we have to be more careful with 0-sized objects at addresses that can conflict with memory that LLVM does "own". For example, in the following case, I think that LLVM should be allowed to fold the comparison here to true, even if let x: &() = &*(0x80000000 as *const u8);
let y: [u8; 2] = 0;
let _t = *x; // force `x` to be valid.
let x_addr = x as *const () as usize;
let y_addr = (&y) as *const () as usize;
// `y` is a stack object, and `x` is a valid object, therefore `x` and `y` must be disjoint.
if x_addr.wrapping_sub(y_addr) >= mem::size_of::<[u8; 2]>() {
something_involving(x_addr, y_addr);
} This means that we might have UB if we use |
I am not sure what exactly you mean by "optimization chicken". However:
Pointer comparison is a whole different can of worms, and lucky enough not involved in the original question. ;) But since you did your comparison at integer type, I disagree with your analysis. Integer comparison should compare the "actual" address. |
I don't know. I haven't experienced any miscompilation myself; I came across this issue while reading the
A clarification: the current implementation does perform a nonzero offset but only when Fwiw, the current implementation calls
@RalfJung Thanks for the article! It's really helpful for understanding what kinds of optimizations LLVM might want to perform, and, as a result, why constraints on offsets are important. It seems like the primary goal of placing constraints on offsets is to be able to guarantee in certain cases that pointers don't alias. If I understand the article correctly, under the proposed model, casting an integer to a pointer (as in The article is slightly ambiguous on the current behavior of LLVM, but I interpret it to mean that LLVM follows the "wildcard provenance" strategy for integer-derived pointers (section 2.4) with additional support for @arielb1 You seem to be saying something like the following: "LLVM cannot assume that it knows about all allocations. For example, we can write a shared library in Rust, compiled with LLVM, with a function following the C ABI, and C/Haskell/Python/etc. can allocate an object and pass a pointer to that object to our library's function; LLVM would never be aware of that allocation. Therefore, LLVM must assume that any Following this line of reasoning, the only evidence that would allow LLVM to conclude that an
Case 3 is concerning because However, assuming LLVM does accept zero-sized allocations as existing, the only remaining case this explanation doesn't fully justify is if the pointer overlaps an allocation created on the LLVM side of the ABI. In this case, the base and offset would in fact be "in bounds" addresses of an allocated object, but LLVM might not allow pointers to its allocations to be created from integers this way. Since LLVM provides an My conclusion from @RalfJung's article is that offsetting a pointer by zero bytes is probably always okay, regardless of the value of that pointer or how it was constructed. @arielb1's explanation provides additional support to this conclusion if LLVM accepts zero-sized allocations as existing. It would be nice to get confirmation from LLVM on this. How's the best way to get an answer? Ask on the llvm-dev mailing list? Thoughts? |
Ah thanks, I had missed that indeed! I sent a PR to miri to enforce this restriction, and it seems in all the code it tests (which is not terribly much), this holds true.
Exactly.
Well, LLVM is mostly ambiguous about its semantics, so it's hard to say anything. ;) But we think that our proposed semantics is consistent with what LLVM does currently, modulo the changes described in the paper.
No, that's not correct. What we call a "logical" pointer in the paper does immediate bounds-checking, even if you offset by 0. So this is still illegal: let x = Box::into_raw(Box::new(0u32));
let x = x.wrapping_offset(8); // okay, this has no inbounds tag
let x = unsafe { x.offset(0) }; // UB, the pointer is not inbounds of the only object it can point to The reason this check is deferred to "physical" pointers is that we cannot know which object it is intended to point to. We could look up memory at this point and see which object lives at the given address, but then
That's worth a try :) |
I don't think LLVM semantics are defined enough to decide whether it is the case or not. From what I know about LLVM optimizations, LLVM does handle 0-sized allocations.
The code dereferences the If zero-sized allocations can't overlap with stack addresses, there's no valid execution in which
We can have the semantics in which physical pointers have deferred bounds checking and there is no way to check whether zero-sized pointers point to a valid zero-sized allocation (in which case, zero-sized allocations have no effect), or we can have a world when there is such a way and there are zero-sized allocations. In both cases, |
Because why? Could have been allocated by something else. AFAIK the only address that will never be in-bounds is Or are we now talking about the code in libstd again, and some some unknown code that might make extra assumptions about the platform it is running on? (You seem to jump between talking about different pieces of code, and I have a hard time following.)
This is based on "cannot guess stack addresses", which is an extremely fuzzy part of the semantics and hard to say anything precise about. Also you assume that dereferencing a pointer to a ZST is not a NOP. It currently is a NOP, and IMO that makes lots of sense. If it's not a NOP, it would still have to be a special case in
Now you lost me entirely. Why does |
Ah, yes, I forgot about I haven't had a chance yet to ask about this question on the LLVM mailing list. I just realized that even if |
Good catch. Yes. Phrasing this in a way that's not confusing will not be completely easy. Do you want to give it a try? |
Eugh… that sounds like a really arcane rule to have to put in the documentation. I guess it's not unreasonable, since even if you're intentionally creating an invalid pointer for a zero-sized type, it's hard to think of a reason you'd pick such a convoluted path to create it – that is, starting with a valid object and |
An exception for 0 specifically would be bad because either you
|
I rather dislike the idea that we might leak LLVM's pointer rules into Rust here. Would this sort of thing be enforced by MIRI, for instance? |
Why that? The rules are actually fairly reasonable, I think.
Yes. miri currently enforces my understanding of these rules, which is that |
I've thought about how to document
The two examples are the cases that I think people care the most about. With this approach, the full description of the constraints on It may be helpful to provide examples, too, and talk about physical pointers that are dereferenced (shown below). However, I lean towards just saying "read the docs for
@RalfJung As I was thinking about this, I had a few questions about the paper:
|
I think this is rather confusing.
I am not even sure what you mean by this. "original pointer cast from the integer"? Also, it seems the game is still open whether So, under that reading, the only pointers that are not okay for size 0 are pointers computed from an allocation, but outside the bounds of that allocation. Maybe that is easier to explain? Making a positive statement is harder, I would say something like
I agree with deferring to
AFAIK there is a dialogue with some LLVM people about this, but unfortunately there are still some open problems with our model, and it will likely be hard to convince LLVM to merge something that has a (tiny) negative peformance impact and doesn't solve all known problems. My coauthors are in closer contact with the LLVM community than I am, so I do not know the details here.
Yes and no. Even on those machine, the "LLVM virtual machine" (pun fully intended ;) can be defined to have more memory than the actual machines have, so that wasting half the address space is not a problem if pointers are large enough. The problem are not machines without MMU, the problem are architectures with small pointer sizes (32bit and less). We do not have a good answer to that. OOM remains an unsolved problem in many ways, e.g. removing an unused I leave you with this quiz. Is the compiler allowed to do what LLVM does here? (GCC does not perform this optimization, it seems.)
Unfortunately we cannot make such an assumption. Or rather, your statement is not even well-typed. The C ABI talks about registers and stuff, it works on the wrong level of abstraction to even ask what happens with provenance information. We'd have to define what it means to link two programs in the C abstract machine (or the LLVM abstract machine, or the Rust abstract machine) for the question of provenance to come up. Practically speaking, when you are linking two object files without LTO, you are doing assembly-level linking and the assembly semantics defines what happens. But when you are linking two LLVM IR files, you are doing LLVM-level linking and the LLVM IR semantics apply.
You cannot make such an assumption. The only way you can make such assumption is if you are providing an assembly object file with no LLVM IR to your users, and you know that your users will only ever interact with that -- at that point assembly semantics govern the behavior. But if your deliverable is LLVM IR or even Rust, then the outside world can interact with you on that level. Practically speaking, you cannot make the assumption because if your function is polymorphic, has the |
Coming back to the original issue here: Is Only once we made that decision, we can talk about how we can improve the documentation of |
Corresponding discussion in the UCG repo (where I am trying to centralize such issues, they get lost too easy here): rust-lang/unsafe-code-guidelines#93 |
getelementptr inbounds
with offset 0
Closing in favor of rust-lang/unsafe-code-guidelines#93. |
As far as I can tell, slicing a
Vec<T>
(in safe code) results in undefined behavior whenT
is zero-sized or theVec
has zero capacity. I'm probably missing something, but I'm creating this issue in case my investigation is correct.In particular, these two examples appear to cause undefined behavior due the
.offset()
call violating the first safety constraint ("Both the starting and resulting pointer must be either in bounds or one byte past the end of the same allocated object.") when performing the slice:Example 1
In the first example, the
v
has field values(Verify this with
v.as_ptr()
andv.len()
.) Performing the slice&v[2..3]
expands to approximately the following:So, it's calling
ptr.offset(2)
whereptr
has value0x1
. This pointer is not "in bounds or one byte past the end of [an] allocated object", so the.offset()
is undefined behavior. (This pointer was created from casting an integer (the alignment of()
) to a pointer inlibcore/ptr.rs, Unique::empty
.)Example 2
The second example has a similar issue. In the second example, the
v
has field values(Verify this with
v.as_ptr()
andv.len()
.) Performing the slice&v[0..0]
expands to approximately the following:So, it's calling
ptr.offset(0)
whereptr
has value0x4
. This pointer is not "in bounds or one byte past the end of [an] allocated object", so the.offset()
is undefined behavior. (This pointer was created from casting an integer (the alignment ofi32
) to a pointer inlibcore/ptr.rs, Unique::empty
.)Further investigation
There are a few ways that these examples might actually not be undefined behavior:
If the documentation is incorrect, and
.offset()
is in fact safe if the offset in bytes is zero (even if the pointer is not part of an allocated object).If LLVM considers
Unique::empty
to be an allocator so that the returned pointer is considered part of an allocated object. I don't see anything to indicate this is the case, though.If, somewhere, the runtime allocates the range of bytes with addresses
0x1..=(max possible alignment)
. This would mean that pointers returned byUnique::empty
would be within an allocated object. I don't see anything to indicate this is the case, though, and I'm not entirely convinced that casting an integer to a pointer would work in this case anyway (since the pointer would be derived from an integer instead of offsetting a pointer of an existing allocation).I did some further investigation into possibility 1.
The
.offset()
method is converted into an LLVMgetelementptr inbounds
instruction. (src/libcore/ptr.rs
provides the.offset()
method, which callsintrinsics::offset
.src/libcore/intrinsics.rs
defines theextern "rust-intrinsic"
offset
but not the implementation. Thecodegen_intrinsic_call
function insrc/librustc_codegen_llvm/intrinsic.rs
handles the"offset"
case by calling.inbounds_gep()
in theBuilder
. The implementation of.inbounds_gep()
is provided insrc/librustc_codegen_llvm/builder.rs
, which in turn calls theextern
functionLLVMBuildInBoundsGEP
imported insrc/librustc_llvm/ffi.rs
. The function is defined insrc/llvm/include/llvm-c/Core.h
)The docs for the LLVM
getelementptr inbounds
instruction say the following:The LLVM docs say this about poison values:
As far as I can tell, the reason why the Rust docs for
.offset()
consider getting a "poison value" to be undefined behavior is that performing any operation with a dependence on the poison value (e.g. printing it withprintln!
) is undefined behavior. In particular, it's possible to perform operations with a dependence on a pointer value in safe code, so a pointer must never be a poison value.Anyway, back to the safety constraints on
.offset()
. The constraints listed in the docs forgetelementptr inbounds
match the constraints listed in the docs for.offset()
with one exception: "The only in bounds address for a null pointer in the default address-space is the null pointer itself." This means that even though a null pointer is not part of an allocation, it's still safe to perform an offset of 0 bytes on it. The docs forgetelementptr inbounds
don't indicate that this is true for non-null pointers, though, which is the case described in this issue (slicing aVec
with zero-size elements or zero capacity).Meta
This appears to be an issue in both stable (1.29.1) and nightly.
The text was updated successfully, but these errors were encountered: