I have a question (or rather a series of questions) about the behavior of getelementptr inbounds
(GEPi) when the effective offset is 0. In the following, I will assume that it does not matter how that offset is computed – it might be array index 0, or a field at offset 0, or an arbitrary array index when the array element type has size 0. When I say that GEPi is “well-defined”, I mean that it does not return poison
.
Also note that I am asking about the dynamic semantics of LLVM IR here, so basically I am asking what an omniscient optimizer would be allowed to do with such code. So, this is not about whether the offset is “known” to be 0 or not.
(I have checked the GEP FAQ and could not see my question answered there.)
Stage 0: null pointers
Based on the LangRef (“The only in bounds address for a null pointer in the default address-space is the null pointer itself.”), I assume the following is well-defined:
%ptr = i8* null
%res = getelementptr inbounds i8, i8* %ptr, i64 0
After all, both the input to GEPi (null
) and its output (null
) are in bounds of the same allocation (according to what I quoted above).
Stage 1: literal integer pointers
In Rust, we also heavily rely on the following being well-defined (for any integer constant N):
%ptr = inttoptr (i64 N to i8*)
%res = getelementptr inbounds i8, i8* %ptr, i64 0
Folklore has it that someone in the past talked to LLVM people to get “blessing” for this pattern, but the details of that have been lost to time and (as far as I can tell) this never made it into the LangRef. Can someone confirm that this is indeed well-defined – or, if this is considered “ill-defined by omission”, can this be made well-defined by a LangRef patch without having to change optimizations? I would hope so, both to ensure Rust is using LLVM correctly, and because at least to me this seems very similar to the NULL case.
Stage 2: dangling and out-of-bounds pointers
If the first two stages are fine, then the question comes up whether there are any pointers for which GEPi with offset 0 is not well-defined. In particular, is the following well-defined?
// Create a dangling pointer (i.e., used to point to
// an allocation but that allocation is gone)
%ptr = call i8* @malloc(i64 4)
call void @free(i8* %ptr)
// Now index into that
%res = getelementptr inbounds i8, i8* %ptr, i64 0
I am aware that the corresponding C code is definitely UB due to “pointer zapping” (pointer values become indeterminate when the allocation they point to is deallocated), but I am asking about LLVM semantics here, and to my knowledge nothing in the LLVM semantics indicates that LLVM has “pointer zapping”.
And what about the following?
// Create an out-of-bounds pointer
%base = call i8* @malloc(i64 2)
%ptr = getelementptr i8, i8* %ptr, i64 4
// Now offset that
%res = getelementptr inbounds i8, i8* %ptr, i64 0
The latter is fairly clearly not well-defined under the current LangRef. But given in particular that even null pointers can be offset by 0, it seems possible to me that optimizations could be fine with allowing any pointer to be offset by 0. Does that sound like a reasonable option for LLVM?
For Rust, it might be quite useful if this were well-defined, since it would let us remove some awkward special cases from our documentation. Right now, we allow offset(0)
on pointers created by “casting any non-zero integer literal to a pointer, even if some memory happens to exist at that address and gets deallocated”, but we cannot allow it on all dangling pointers due to LLVM IR semantics. This regularly leads to confusion because it is hard to explain this rule.
(If LLVM maintains that GEPi by 0 can return poison
for non-poison
inputs, we could of course alternatively change the Rust compiler to remove inbounds
whenever the offset might be 0, but that cannot always be statically predicted and it also seems rather fragile.)
@nlopes have you experimented with things like this in Alive?
I wonder how “always allow 0-offset in GEPi” would fare there.