This is an archive of the discontinued LLVM Phabricator instance.

[ConstantFold] Don't convert getelementptr to ptrtoint+inttoptr
ClosedPublic

Authored by nikic on Apr 29 2022, 8:32 AM.

Details

Summary

ConstantFolding currently converts "getelementptr i8, Ptr, (sub 0, V)" to "inttoptr (sub (ptrtoint Ptr), V)". This transform is, taken by itself, correct, but does came with two issues:

  1. It unnecessarily broadens provenance by introducing an inttoptr. We generally prefer not to introduce inttoptr during optimization.
  2. For the case where V == ptrtoint Ptr, this folds to inttoptr 0, which further folds to null. In that case provenance becomes incorrect. This has been observed as a real-world miscompile with rustc.

We should probably address that incorrect inttoptr 0 fold at some point, but in either case we should also drop this inttoptr-introducing fold. Instead, replace it with a fold rooted at ptrtoint(getelementptr), which seems to cover the original motivation for this fold (test2 in the changed file).

Diff Detail

Event Timeline

nikic created this revision.Apr 29 2022, 8:32 AM
nikic requested review of this revision.Apr 29 2022, 8:32 AM
Herald added a project: Restricted Project. · View Herald TranscriptApr 29 2022, 8:32 AM
nikic updated this revision to Diff 426067.Apr 29 2022, 8:36 AM

Rebase over test change.

nlopes accepted this revision.Apr 29 2022, 9:10 AM

Sounds great, thanks! We should avoid introducing ptr2int at all costs.

Note for other reviewers: the new code is covered by existing tests. It's there to avoid regressing on cases where no int2ptr is introduced on new pointers, but we can still get rid of the gep. I think keeping this case is find as it doesn't escape more than the original code.

This revision is now accepted and ready to land.Apr 29 2022, 9:10 AM
tschuett added inline comments.
llvm/lib/Analysis/ConstantFolding.cpp
879

Could you deprecate or warn on using this expression?

nlopes added inline comments.Apr 29 2022, 12:06 PM
llvm/lib/Analysis/ConstantFolding.cpp
879

It's not deprecated. It is used. It's just that it should be used as few times as possible.

I agree that introducing ptrtoint + inttoptr here doesn't sound like a good idea because both it is bad for alias analysis and its correctness is not clear.

For the case where V == ptrtoint Ptr, this folds to inttoptr 0, which further folds to null. In that case provenance becomes incorrect. This has been observed as a real-world miscompile with rustc.

If LLVM is using the definition of null pointer in C, inttoptr 0 must be null, implying that folding gep p, -(ptrtoint p) to null must be the problematic one.

C17, 6.3.2.3.p3. An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.

This has been observed as a real-world miscompile with rustc.

Could you share a link to the bug report please?

nikic added a comment.Apr 30 2022, 4:31 AM

I agree that introducing ptrtoint + inttoptr here doesn't sound like a good idea because both it is bad for alias analysis and its correctness is not clear.

For the case where V == ptrtoint Ptr, this folds to inttoptr 0, which further folds to null. In that case provenance becomes incorrect. This has been observed as a real-world miscompile with rustc.

If LLVM is using the definition of null pointer in C, inttoptr 0 must be null, implying that folding gep p, -(ptrtoint p) to null must be the problematic one.

C17, 6.3.2.3.p3. An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.

I don't think this follows: This is talking about "integer constant expressions", which are a front-end concern. It means that the front-end is required to match for (void*)0 and emit that as ptr null rather than inttoptr (i64 0 to ptr). At least from that wording, doing int x = 0; (void*)xdoes not result in a null pointer (though possibly other wording implies that?)

I don't think having ptrtoint 1 have universal provenance and ptrtoint 0 have nullary provenance can lead to consistent semantics. It renders many transforms that are "obviously correct" illegal, such as:

define ptr @src(ptr %p, i64 %idx) {
  %p2 = getelementptr i8, ptr %p, i64 %idx
  ret ptr %p2
}
define ptr @tgt(ptr %p, i64 %idx) {
  %p.int = ptrtoint ptr %p to i64
  %p.add = add i64 %p.int, %idx
  %p2 = inttoptr i64 %p.add to ptr
  ret ptr %p2
}

While this transform is very undesirable, it should be correct because it only increases provenance. However, due to the special ptrtoint 0 handling this is incorrect for the special case where p.int == -idx.

This has been observed as a real-world miscompile with rustc.

Could you share a link to the bug report please?

https://github.com/rust-lang/rust/pull/96538

I don't think this follows: This is talking about "integer constant expressions", which are a front-end concern. It means that the front-end is required to match for (void*)0 and emit that as ptr null rather than inttoptr (i64 0 to ptr). At least from that wording, doing int x = 0; (void*)xdoes not result in a null pointer (though possibly other wording implies that?)

I don't think having ptrtoint 1 have universal provenance and ptrtoint 0 have nullary provenance can lead to consistent semantics. It renders many transforms that are "obviously correct" illegal, such as:

define ptr @src(ptr %p, i64 %idx) {
  %p2 = getelementptr i8, ptr %p, i64 %idx
  ret ptr %p2
}
define ptr @tgt(ptr %p, i64 %idx) {
  %p.int = ptrtoint ptr %p to i64
  %p.add = add i64 %p.int, %idx
  %p2 = inttoptr i64 %p.add to ptr
  ret ptr %p2
}

While this transform is very undesirable, it should be correct because it only increases provenance. However, due to the special ptrtoint 0 handling this is incorrect for the special case where p.int == -idx.

One alternative solution is to define a null pointer as having universal provenance.
This is against the LangRef wording A null pointer in the default address-space is associated with no address. (meaning that null + x must not be able to access anything), but explains these:

  • Replacing a pointer into NULL is supported because it makes the program more defined.
if (x == null) {
  f(x); -> f(null);
}
  • Folding inttoptr 0 to null is naturally explained.

Instead, we will lose optimization opportunities on gep null, x. However, alias analysis can still support gep inbounds null, x because the users of this pointer must ensure that range [null, null+x] are inbounds, and no allocation is located at null.
Therefore, it comes to preserving the flag inbounds as much as possible.

IMO the semantics of NULL pointer is also in the gray area; didn't mean to be against this patch, but wanted to claim that inttoptr 0 -> NULL might be another issue.