Skip to content

Conversation

@nikic
Copy link
Contributor

@nikic nikic commented Sep 19, 2024

If a dereferenceability fact is provided through !dereferenceable (or similar), it may only hold on the given control flow path. When we use isSafeToSpeculativelyExecute() to check multiple instructions, we might make use of !dereferenceable information that does not hold at the speculation target. This doesn't happen when speculating instructions one by one, because !dereferenceable will be dropped while speculating.

Fix this by checking whether the instruction with !dereferenceable dominates the context instruction. If this is not the case, it means we are speculating, and cannot guarantee that it holds at the speculation target.

Fixes #108854.

@llvmbot llvmbot added llvm:analysis Includes value tracking, cost tables and constant folding llvm:transforms labels Sep 19, 2024
@llvmbot
Copy link
Member

llvmbot commented Sep 19, 2024

@llvm/pr-subscribers-clang

@llvm/pr-subscribers-llvm-analysis

Author: Nikita Popov (nikic)

Changes

If a dereferenceability fact is provided through !dereferenceable (or similar), it may only hold on the given control flow path. When we use isSafeToSpeculativelyExecute() to check multiple instructions, we might make use of !dereferenceable information that does not hold at the speculation target. This doesn't happen when speculating instructions one by one, because !dereferenceable will be dropped while speculating.

Fix this by checking whether the instruction with !dereferenceable dominates the context instruction. If this is not the case, it means we are speculating, and cannot guarantee that it holds at the speculation target.

Fixes #108854.


Full diff: https://github.com/llvm/llvm-project/pull/109277.diff

4 Files Affected:

  • (modified) llvm/lib/Analysis/Loads.cpp (+11)
  • (modified) llvm/lib/Analysis/MemDerefPrinter.cpp (+2-2)
  • (modified) llvm/lib/CodeGen/MachineOperand.cpp (+2-1)
  • (modified) llvm/test/Transforms/SimplifyCFG/speculate-derefable-load.ll (+7-4)
diff --git a/llvm/lib/Analysis/Loads.cpp b/llvm/lib/Analysis/Loads.cpp
index 957ac883490c45..11f3807ffacf6e 100644
--- a/llvm/lib/Analysis/Loads.cpp
+++ b/llvm/lib/Analysis/Loads.cpp
@@ -104,6 +104,17 @@ static bool isDereferenceableAndAlignedPointer(
     if (CheckForNonNull &&
         !isKnownNonZero(V, SimplifyQuery(DL, DT, AC, CtxI)))
       return false;
+    // When using something like !dereferenceable on a load, the
+    // dereferenceability may only be valid on a specific control-flow path.
+    // If the instruction doesn't dominate the context instruction, we're
+    // asking about dereferenceability under the assumption that the
+    // instruction has been speculated to the point of the context instruction,
+    // in which case we don't know if the dereferenceability info still holds.
+    // We don't bother handling allocas here, as they aren't speculatable
+    // anyway.
+    auto *I = dyn_cast<Instruction>(V);
+    if (I && !isa<AllocaInst>(I))
+      return CtxI && isValidAssumeForContext(I, CtxI, DT);
     return true;
   };
   if (IsKnownDeref()) {
diff --git a/llvm/lib/Analysis/MemDerefPrinter.cpp b/llvm/lib/Analysis/MemDerefPrinter.cpp
index e858d941435441..68cb8859488f70 100644
--- a/llvm/lib/Analysis/MemDerefPrinter.cpp
+++ b/llvm/lib/Analysis/MemDerefPrinter.cpp
@@ -30,10 +30,10 @@ PreservedAnalyses MemDerefPrinterPass::run(Function &F,
   for (auto &I : instructions(F)) {
     if (LoadInst *LI = dyn_cast<LoadInst>(&I)) {
       Value *PO = LI->getPointerOperand();
-      if (isDereferenceablePointer(PO, LI->getType(), DL))
+      if (isDereferenceablePointer(PO, LI->getType(), DL, LI))
         Deref.push_back(PO);
       if (isDereferenceableAndAlignedPointer(PO, LI->getType(), LI->getAlign(),
-                                             DL))
+                                             DL, LI))
         DerefAndAligned.insert(PO);
     }
   }
diff --git a/llvm/lib/CodeGen/MachineOperand.cpp b/llvm/lib/CodeGen/MachineOperand.cpp
index 6ee47624f31c54..89d32c3f005e00 100644
--- a/llvm/lib/CodeGen/MachineOperand.cpp
+++ b/llvm/lib/CodeGen/MachineOperand.cpp
@@ -1047,7 +1047,8 @@ bool MachinePointerInfo::isDereferenceable(unsigned Size, LLVMContext &C,
     return false;
 
   return isDereferenceableAndAlignedPointer(
-      BasePtr, Align(1), APInt(DL.getPointerSizeInBits(), Offset + Size), DL);
+      BasePtr, Align(1), APInt(DL.getPointerSizeInBits(), Offset + Size), DL,
+      dyn_cast<Instruction>(BasePtr));
 }
 
 /// getConstantPool - Return a MachinePointerInfo record that refers to the
diff --git a/llvm/test/Transforms/SimplifyCFG/speculate-derefable-load.ll b/llvm/test/Transforms/SimplifyCFG/speculate-derefable-load.ll
index 8c7afa4598bd4b..0138433312ed84 100644
--- a/llvm/test/Transforms/SimplifyCFG/speculate-derefable-load.ll
+++ b/llvm/test/Transforms/SimplifyCFG/speculate-derefable-load.ll
@@ -77,14 +77,17 @@ exit:
   ret i64 %res
 }
 
-; FIXME: This is a miscompile.
 define i64 @deref_no_hoist(i1 %c, ptr align 8 dereferenceable(8) %p1) {
 ; CHECK-LABEL: define i64 @deref_no_hoist(
 ; CHECK-SAME: i1 [[C:%.*]], ptr align 8 dereferenceable(8) [[P1:%.*]]) {
-; CHECK-NEXT:  [[ENTRY:.*:]]
-; CHECK-NEXT:    [[P2:%.*]] = load ptr, ptr [[P1]], align 8, !align [[META0:![0-9]+]]
+; CHECK-NEXT:  [[ENTRY:.*]]:
+; CHECK-NEXT:    br i1 [[C]], label %[[IF:.*]], label %[[EXIT:.*]]
+; CHECK:       [[IF]]:
+; CHECK-NEXT:    [[P2:%.*]] = load ptr, ptr [[P1]], align 8, !dereferenceable [[META0:![0-9]+]], !align [[META0]]
 ; CHECK-NEXT:    [[V:%.*]] = load i64, ptr [[P2]], align 8
-; CHECK-NEXT:    [[RES:%.*]] = select i1 [[C]], i64 [[V]], i64 0
+; CHECK-NEXT:    br label %[[EXIT]]
+; CHECK:       [[EXIT]]:
+; CHECK-NEXT:    [[RES:%.*]] = phi i64 [ [[V]], %[[IF]] ], [ 0, %[[ENTRY]] ]
 ; CHECK-NEXT:    ret i64 [[RES]]
 ;
 entry:

@llvmbot
Copy link
Member

llvmbot commented Sep 19, 2024

@llvm/pr-subscribers-llvm-transforms

Author: Nikita Popov (nikic)

Changes

If a dereferenceability fact is provided through !dereferenceable (or similar), it may only hold on the given control flow path. When we use isSafeToSpeculativelyExecute() to check multiple instructions, we might make use of !dereferenceable information that does not hold at the speculation target. This doesn't happen when speculating instructions one by one, because !dereferenceable will be dropped while speculating.

Fix this by checking whether the instruction with !dereferenceable dominates the context instruction. If this is not the case, it means we are speculating, and cannot guarantee that it holds at the speculation target.

Fixes #108854.


Full diff: https://github.com/llvm/llvm-project/pull/109277.diff

4 Files Affected:

  • (modified) llvm/lib/Analysis/Loads.cpp (+11)
  • (modified) llvm/lib/Analysis/MemDerefPrinter.cpp (+2-2)
  • (modified) llvm/lib/CodeGen/MachineOperand.cpp (+2-1)
  • (modified) llvm/test/Transforms/SimplifyCFG/speculate-derefable-load.ll (+7-4)
diff --git a/llvm/lib/Analysis/Loads.cpp b/llvm/lib/Analysis/Loads.cpp
index 957ac883490c45..11f3807ffacf6e 100644
--- a/llvm/lib/Analysis/Loads.cpp
+++ b/llvm/lib/Analysis/Loads.cpp
@@ -104,6 +104,17 @@ static bool isDereferenceableAndAlignedPointer(
     if (CheckForNonNull &&
         !isKnownNonZero(V, SimplifyQuery(DL, DT, AC, CtxI)))
       return false;
+    // When using something like !dereferenceable on a load, the
+    // dereferenceability may only be valid on a specific control-flow path.
+    // If the instruction doesn't dominate the context instruction, we're
+    // asking about dereferenceability under the assumption that the
+    // instruction has been speculated to the point of the context instruction,
+    // in which case we don't know if the dereferenceability info still holds.
+    // We don't bother handling allocas here, as they aren't speculatable
+    // anyway.
+    auto *I = dyn_cast<Instruction>(V);
+    if (I && !isa<AllocaInst>(I))
+      return CtxI && isValidAssumeForContext(I, CtxI, DT);
     return true;
   };
   if (IsKnownDeref()) {
diff --git a/llvm/lib/Analysis/MemDerefPrinter.cpp b/llvm/lib/Analysis/MemDerefPrinter.cpp
index e858d941435441..68cb8859488f70 100644
--- a/llvm/lib/Analysis/MemDerefPrinter.cpp
+++ b/llvm/lib/Analysis/MemDerefPrinter.cpp
@@ -30,10 +30,10 @@ PreservedAnalyses MemDerefPrinterPass::run(Function &F,
   for (auto &I : instructions(F)) {
     if (LoadInst *LI = dyn_cast<LoadInst>(&I)) {
       Value *PO = LI->getPointerOperand();
-      if (isDereferenceablePointer(PO, LI->getType(), DL))
+      if (isDereferenceablePointer(PO, LI->getType(), DL, LI))
         Deref.push_back(PO);
       if (isDereferenceableAndAlignedPointer(PO, LI->getType(), LI->getAlign(),
-                                             DL))
+                                             DL, LI))
         DerefAndAligned.insert(PO);
     }
   }
diff --git a/llvm/lib/CodeGen/MachineOperand.cpp b/llvm/lib/CodeGen/MachineOperand.cpp
index 6ee47624f31c54..89d32c3f005e00 100644
--- a/llvm/lib/CodeGen/MachineOperand.cpp
+++ b/llvm/lib/CodeGen/MachineOperand.cpp
@@ -1047,7 +1047,8 @@ bool MachinePointerInfo::isDereferenceable(unsigned Size, LLVMContext &C,
     return false;
 
   return isDereferenceableAndAlignedPointer(
-      BasePtr, Align(1), APInt(DL.getPointerSizeInBits(), Offset + Size), DL);
+      BasePtr, Align(1), APInt(DL.getPointerSizeInBits(), Offset + Size), DL,
+      dyn_cast<Instruction>(BasePtr));
 }
 
 /// getConstantPool - Return a MachinePointerInfo record that refers to the
diff --git a/llvm/test/Transforms/SimplifyCFG/speculate-derefable-load.ll b/llvm/test/Transforms/SimplifyCFG/speculate-derefable-load.ll
index 8c7afa4598bd4b..0138433312ed84 100644
--- a/llvm/test/Transforms/SimplifyCFG/speculate-derefable-load.ll
+++ b/llvm/test/Transforms/SimplifyCFG/speculate-derefable-load.ll
@@ -77,14 +77,17 @@ exit:
   ret i64 %res
 }
 
-; FIXME: This is a miscompile.
 define i64 @deref_no_hoist(i1 %c, ptr align 8 dereferenceable(8) %p1) {
 ; CHECK-LABEL: define i64 @deref_no_hoist(
 ; CHECK-SAME: i1 [[C:%.*]], ptr align 8 dereferenceable(8) [[P1:%.*]]) {
-; CHECK-NEXT:  [[ENTRY:.*:]]
-; CHECK-NEXT:    [[P2:%.*]] = load ptr, ptr [[P1]], align 8, !align [[META0:![0-9]+]]
+; CHECK-NEXT:  [[ENTRY:.*]]:
+; CHECK-NEXT:    br i1 [[C]], label %[[IF:.*]], label %[[EXIT:.*]]
+; CHECK:       [[IF]]:
+; CHECK-NEXT:    [[P2:%.*]] = load ptr, ptr [[P1]], align 8, !dereferenceable [[META0:![0-9]+]], !align [[META0]]
 ; CHECK-NEXT:    [[V:%.*]] = load i64, ptr [[P2]], align 8
-; CHECK-NEXT:    [[RES:%.*]] = select i1 [[C]], i64 [[V]], i64 0
+; CHECK-NEXT:    br label %[[EXIT]]
+; CHECK:       [[EXIT]]:
+; CHECK-NEXT:    [[RES:%.*]] = phi i64 [ [[V]], %[[IF]] ], [ 0, %[[ENTRY]] ]
 ; CHECK-NEXT:    ret i64 [[RES]]
 ;
 entry:

If a dereferenceability fact is provided through `!dereferenceable`,
it may only hold on the given control flow path. When we use
`isSafeToSpeculativelyExecute()` to check multiple instructions, we
might make use of `!dereferenceable` information that does not
hold at the speculation target. This doesn't happen when speculating
instructions one by one, because `!dereferenceable` will be dropped
while speculating.

Fix this by checking whether the instruction with `!dereferenceable`
dominates the context instruction. If this is not the case, it means
we are speculating, and cannot guarantee that it holds at the
speculation target.

Fixes llvm#108854.
@llvmbot llvmbot added the clang Clang issues not falling into any other category label Sep 19, 2024
// in which case we don't know if the dereferenceability info still holds.
// We don't bother handling allocas here, as they aren't speculatable
// anyway.
auto *I = dyn_cast<Instruction>(V);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (auto *I = dyn_cast<Instruction>(V))
  if (!isa<AllocaInst>(I))
     return CtxI && isValidAssumeForContext(I, CtxI, DT);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (auto *I = dyn_cast<Instruction>(V))
  if (!isa<AllocaInst>(I))
    if (CtxI == nullptr)
      return false;
    else
     return isValidAssumeForContext(I, CtxI, DT);

@efriedma-quic
Copy link
Collaborator

Please update the documentation for isSafeToSpeculativelyExecute() to specify the semantics in the case where the operands of the instruction don't dominate CtxI.

@nikic
Copy link
Contributor Author

nikic commented Sep 20, 2024

I added some wording to isSafeToSpeculativelyExecute(), but not sure if this is what you had in mind.

@efriedma-quic
Copy link
Collaborator

Yes, that makes sense, thanks.

Copy link
Member

@dtcxzyw dtcxzyw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch!

@nikic nikic merged commit 5a4c6f9 into llvm:main Sep 23, 2024
4 checks passed
@nikic nikic deleted the spec-deref branch September 23, 2024 07:13
@danilaml
Copy link
Collaborator

danilaml commented Nov 4, 2024

Is it even possible for isSafeToSpeculativelyExecute to return true now with the default (nullptr) CtxI for loads? Can isDereferenceableAndAlignedPointer just short-circuit to false with null CtxI?

@nikic
Copy link
Contributor Author

nikic commented Nov 4, 2024

Is it even possible for isSafeToSpeculativelyExecute to return true now with the default (nullptr) CtxI for loads? Can isDereferenceableAndAlignedPointer just short-circuit to false with null CtxI?

Yes, it's possible for anything where the derefability is not context-sensitive (like globals, dereferenceable arguments, allocas, etc).

@danilaml
Copy link
Collaborator

danilaml commented Nov 4, 2024

@nikic I mean not in theory but currently. I don't see those (except allocas) handled anywhere unless I'm missing something?

@nikic
Copy link
Contributor Author

nikic commented Nov 4, 2024

@nikic I mean not in theory but currently. I don't see those (except allocas) handled anywhere unless I'm missing something?

The I && part handles those. If it's a global or argument (thus not an instruction) we'll fall through to the return true.

@danilaml
Copy link
Collaborator

danilaml commented Nov 4, 2024

@nikic I see. Thanks. Probably not going to help me come up with a reproducer for a weird behavior/bug I was seeing with LoopVectorizer where it produced loads from poison pointers when it thought it didn't need to predicate a load (after this patch it always needs to since loads are never safe to speculate in its check but I think it's just masking whatever is going on).

@danilaml
Copy link
Collaborator

danilaml commented Dec 2, 2024

@nikic By the way, are there plans to support allocation functions other than alloca in this check? I don't see currently any llvm passes assigning dereferenceable(_or_null) attribute to something like malloc(42) , but I don't see why not and in that case this should also be something not reliant on the context.

@nikic
Copy link
Contributor Author

nikic commented Dec 2, 2024

@nikic By the way, are there plans to support allocation functions other than alloca in this check? I don't see currently any llvm passes assigning dereferenceable(_or_null) attribute to something like malloc(42) , but I don't see why not and in that case this should also be something not reliant on the context.

I believe we don't mark allocation return values as dereferencable because it would imply a too strong property right now (staying dereferenceable even after the allocation was freed).

@danilaml
Copy link
Collaborator

danilaml commented Dec 2, 2024

@nikic ah, yeah. I remember the issue with dereferenceable w.r.t. free discussion. Guess will have to roll something for allocations that don't have this problem.

@danilaml
Copy link
Collaborator

danilaml commented Dec 6, 2024

@nikic one thing I've discovered with this fact is that now load is not dereferenceable at "load" itself due to isValidAssumeForContext quirk of not allowing assume affect itself. Not sure if it's intentional for attributes here - makes it look like it's not safe to speculate load in its current position. There is also some friction with instruction vs iterator (i.e. when you only have insertion point at the end of bb so can't easily turn it into an instruction), but that's a different matter.

@nikic
Copy link
Contributor Author

nikic commented Dec 6, 2024

@danilaml We could pass AllowEphemerals=true to isValidAssumeForContext.

@danilaml
Copy link
Collaborator

danilaml commented Dec 6, 2024

@nikic Does AllowEphemerals actually do what we want in that case? Haven't looked in detail, but on the surface it seemed way more involved than just if (I == CtxI) return true;.

@danilaml
Copy link
Collaborator

danilaml commented Dec 9, 2024

@nikic also, another situation the current impl doesn't seem to account for is IR like

bb1:
  %p1 = load dereferenceable(N)
...
bb2:
  %p2 = load dereferenceable(M)
...
common:
  %p = phi ptr [%p1, %bb1], [%p2, %bb2]
  %res = load i32 %p ; <-- dereferenceable(min(N,M)) at least

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang Clang issues not falling into any other category llvm:analysis Includes value tracking, cost tables and constant folding llvm:transforms

Projects

None yet

Development

Successfully merging this pull request may close these issues.

isSafeToSpeculativelyExecute seems not safe to use from SimplifyCFG

6 participants