[llvm] [JumpThreading] Freeze undef/poison select condition in unfoldSelectInstr (PR #199408)

Justin Lebar via llvm-commits llvm-commits at lists.llvm.org
Sun May 24 02:11:52 PDT 2026


https://github.com/jlebar created https://github.com/llvm/llvm-project/pull/199408

When unfoldSelectInstr rewrites a select as a conditional branch, it
used SI->getCondition() directly. A select with an undef or poison
condition does not immediately cause UB, but branching on undef or
poison does.

Freeze the condition before using it as a branch input, matching the guard
already used by tryToUnfoldSelectInCurrBB.

This bug was found by a large run of Opus 4.7 looking for bugs in LLVM.


>From 1865362a8c6dbabe9a6e661dd256cd69b7384ba8 Mon Sep 17 00:00:00 2001
From: Justin Lebar <justin.lebar at gmail.com>
Date: Fri, 22 May 2026 00:25:34 -0700
Subject: [PATCH] [JumpThreading] Freeze undef/poison select condition in
 unfoldSelectInstr

When unfoldSelectInstr rewrites a select as a conditional branch, it
used SI->getCondition() directly. A select with an undef or poison
condition does not immediately cause UB, but branching on undef or
poison does.

Freeze the condition before using it as a branch input, matching the guard
already used by tryToUnfoldSelectInCurrBB.

This bug was found by a large run of Opus 4.7 looking for bugs in LLVM.
---
 llvm/lib/Transforms/Scalar/JumpThreading.cpp  | 10 +++-
 .../JumpThreading/branch-debug-info2.ll       | 51 +++++++++++++++++--
 .../stale-loop-info-after-unfold-select.ll    | 51 +++++++++++++++++--
 3 files changed, 103 insertions(+), 9 deletions(-)

diff --git a/llvm/lib/Transforms/Scalar/JumpThreading.cpp b/llvm/lib/Transforms/Scalar/JumpThreading.cpp
index 415136b612ac2..a9a7a87cb8c46 100644
--- a/llvm/lib/Transforms/Scalar/JumpThreading.cpp
+++ b/llvm/lib/Transforms/Scalar/JumpThreading.cpp
@@ -2788,8 +2788,16 @@ void JumpThreadingPass::unfoldSelectInstr(BasicBlock *Pred, BasicBlock *BB,
   // Move the unconditional branch to NewBB.
   PredTerm->removeFromParent();
   PredTerm->insertInto(NewBB, NewBB->end());
+  // A select with an undef or poison condition does not immediately cause UB,
+  // but branching on undef or poison does. Freeze the condition if needed
+  // before using it as a branch input.
+  Value *Cond = SI->getCondition();
+  if (!isGuaranteedNotToBeUndefOrPoison(Cond, nullptr, SI)) {
+    Cond = new FreezeInst(Cond, "cond.fr", SI->getIterator());
+    cast<FreezeInst>(Cond)->setDebugLoc(DebugLoc::getTemporary());
+  }
   // Create a conditional branch and update PHI nodes.
-  auto *BI = CondBrInst::Create(SI->getCondition(), NewBB, BB, Pred);
+  auto *BI = CondBrInst::Create(Cond, NewBB, BB, Pred);
   BI->applyMergedLocation(PredTerm->getDebugLoc(), SI->getDebugLoc());
   BI->copyMetadata(*SI, {LLVMContext::MD_prof});
   SIUse->setIncomingValue(Idx, SI->getFalseValue());
diff --git a/llvm/test/Transforms/JumpThreading/branch-debug-info2.ll b/llvm/test/Transforms/JumpThreading/branch-debug-info2.ll
index 59b6ef51d670f..021b3c0290170 100644
--- a/llvm/test/Transforms/JumpThreading/branch-debug-info2.ll
+++ b/llvm/test/Transforms/JumpThreading/branch-debug-info2.ll
@@ -1,5 +1,31 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
 ; RUN: opt -passes=jump-threading -S %s -o - | FileCheck %s
 define void @prepare_next_shadow() !dbg !7 {
+; CHECK-LABEL: define void @prepare_next_shadow(
+; CHECK-SAME: ) !dbg [[DBG7:![0-9]+]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    br label %[[SHADOW_TO_PTR_EXIT:.*]], !dbg [[DBG9:![0-9]+]]
+; CHECK:       [[SHADOW_TO_PTR_EXIT]]:
+; CHECK-NEXT:    [[SHR_I:%.*]] = lshr i64 0, 12, !dbg [[DBG10:![0-9]+]]
+; CHECK-NEXT:    [[CMP_I:%.*]] = icmp ult i64 [[SHR_I]], undef, !dbg [[DBG11:![0-9]+]]
+; CHECK-NEXT:    br i1 [[CMP_I]], label %[[FOR_INC:.*]], label %[[IF_END_I60:.*]], !dbg [[DBG12:![0-9]+]]
+; CHECK:       [[IF_END_I60]]:
+; CHECK-NEXT:    [[SUB_I:%.*]] = sub i64 [[SHR_I]], undef, !dbg [[DBG13:![0-9]+]]
+; CHECK-NEXT:    [[CMP3_I:%.*]] = icmp ugt i64 [[SUB_I]], 32763, !dbg [[DBG14:![0-9]+]]
+; CHECK-NEXT:    [[CONV7_I:%.*]] = trunc i64 [[SUB_I]] to i32, !dbg [[DBG15:![0-9]+]]
+; CHECK-NEXT:    [[COND_FR:%.*]] = freeze i1 [[CMP3_I]]
+; Jump threading folds the select below into this branch. Ensure debug info is
+; not lost, and is merged from the select and the branch.
+; CHECK-NEXT:    br i1 [[COND_FR]], label %[[FOR_INC]], label %[[PTR_TO_SHADOW_EXIT:.*]], !dbg [[DBG16:![0-9]+]]
+; CHECK:       [[PTR_TO_SHADOW_EXIT]]:
+; CHECK-NEXT:    [[CALL1861:%.*]] = phi i32 [ [[CONV7_I]], %[[IF_END_I60]] ], !dbg [[DBG17:![0-9]+]]
+; CHECK-NEXT:    [[CMP19:%.*]] = icmp slt i32 [[CALL1861]], 0, !dbg [[DBG18:![0-9]+]]
+; CHECK-NEXT:    br i1 [[CMP19]], label %[[FOR_INC]], label %[[IF_END22:.*]], !dbg [[DBG19:![0-9]+]]
+; CHECK:       [[IF_END22]]:
+; CHECK-NEXT:    unreachable, !dbg [[DBG20:![0-9]+]]
+; CHECK:       [[FOR_INC]]:
+; CHECK-NEXT:    br label %[[SHADOW_TO_PTR_EXIT]], !dbg [[DBG21:![0-9]+]]
+;
 entry:
   br label %for.cond, !dbg !9
 
@@ -16,11 +42,6 @@ if.end.i60:                                       ; preds = %shadow_to_ptr.exit
   %cmp3.i = icmp ugt i64 %sub.i, 32763, !dbg !15
   %conv7.i = trunc i64 %sub.i to i32, !dbg !16
   %spec.select.i = select i1 %cmp3.i, i32 -1, i32 %conv7.i, !dbg !17
-; Jump threading is going to fold the select in to the branch. Ensure debug
-; info is not lost, and is merged from the select and the branch.
-; CHECK-NOT: br i1 %cmp3.i, label %for.inc, label %if.end22
-; CHECK: br i1 %cmp3.i, label %for.inc, label %if.end22, !dbg [[DBG:![0-9]+]]
-; CHECK: [[DBG]] = !DILocation(line: 9, column: 1, scope: !{{.*}})
 
   br label %ptr_to_shadow.exit, !dbg !17
 
@@ -68,3 +89,23 @@ for.inc:                                          ; preds = %ptr_to_shadow.exit
 !22 = !DILocation(line: 14, column: 1, scope: !7)
 !23 = !DILocation(line: 15, column: 1, scope: !7)
 !24 = !DILocation(line: 16, column: 1, scope: !7)
+;.
+; CHECK: [[META1:![0-9]+]] = distinct !DICompileUnit(language: DW_LANG_C, file: [[META2:![0-9]+]], producer: "debugify", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, enums: [[META3:![0-9]+]])
+; CHECK: [[META2]] = !DIFile(filename: "{{.*}}jump_threading.ll", directory: {{.*}})
+; CHECK: [[META3]] = !{}
+; CHECK: [[DBG7]] = distinct !DISubprogram(name: "prepare_next_shadow", linkageName: "prepare_next_shadow", scope: null, file: [[META2]], line: 1, type: [[META8:![0-9]+]], scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: [[META1]], retainedNodes: [[META3]])
+; CHECK: [[META8]] = !DISubroutineType(types: [[META3]])
+; CHECK: [[DBG9]] = !DILocation(line: 1, column: 1, scope: [[DBG7]])
+; CHECK: [[DBG10]] = !DILocation(line: 3, column: 1, scope: [[DBG7]])
+; CHECK: [[DBG11]] = !DILocation(line: 4, column: 1, scope: [[DBG7]])
+; CHECK: [[DBG12]] = !DILocation(line: 5, column: 1, scope: [[DBG7]])
+; CHECK: [[DBG13]] = !DILocation(line: 6, column: 1, scope: [[DBG7]])
+; CHECK: [[DBG14]] = !DILocation(line: 7, column: 1, scope: [[DBG7]])
+; CHECK: [[DBG15]] = !DILocation(line: 8, column: 1, scope: [[DBG7]])
+; CHECK: [[DBG16]] = !DILocation(line: 9, column: 1, scope: [[DBG7]])
+; CHECK: [[DBG17]] = !DILocation(line: 12, column: 1, scope: [[DBG7]])
+; CHECK: [[DBG18]] = !DILocation(line: 13, column: 1, scope: [[DBG7]])
+; CHECK: [[DBG19]] = !DILocation(line: 14, column: 1, scope: [[DBG7]])
+; CHECK: [[DBG20]] = !DILocation(line: 15, column: 1, scope: [[DBG7]])
+; CHECK: [[DBG21]] = !DILocation(line: 16, column: 1, scope: [[DBG7]])
+;.
diff --git a/llvm/test/Transforms/JumpThreading/stale-loop-info-after-unfold-select.ll b/llvm/test/Transforms/JumpThreading/stale-loop-info-after-unfold-select.ll
index ba40980c6bf89..086bafe831d5e 100644
--- a/llvm/test/Transforms/JumpThreading/stale-loop-info-after-unfold-select.ll
+++ b/llvm/test/Transforms/JumpThreading/stale-loop-info-after-unfold-select.ll
@@ -13,8 +13,8 @@ define dso_local ptr @func2(ptr %this, ptr) {
 ; CHECK:       while.cond:
 ; CHECK-NEXT:    [[MONTH_0:%.*]] = phi i32 [ undef, [[ENTRY:%.*]] ], [ [[CALL2:%.*]], [[FUNC1_EXIT:%.*]] ], [ [[ADD:%.*]], [[SELECT_UNFOLD:%.*]] ]
 ; CHECK-NEXT:    switch i32 [[MONTH_0]], label [[IF_END_I:%.*]] [
-; CHECK-NEXT:    i32 4, label [[FUNC1_EXIT]]
-; CHECK-NEXT:    i32 1, label [[FUNC1_EXIT]]
+; CHECK-NEXT:      i32 4, label [[FUNC1_EXIT]]
+; CHECK-NEXT:      i32 1, label [[FUNC1_EXIT]]
 ; CHECK-NEXT:    ]
 ; CHECK:       if.end.i:
 ; CHECK-NEXT:    br label [[FUNC1_EXIT]]
@@ -23,7 +23,8 @@ define dso_local ptr @func2(ptr %this, ptr) {
 ; CHECK-NEXT:    [[CALL2]] = tail call signext i32 @func3(i32 signext [[RETVAL_0_I]], i32 signext 1, i32 signext 3)
 ; CHECK-NEXT:    [[CMP:%.*]] = icmp slt i32 [[CALL2]], 1
 ; CHECK-NEXT:    [[ADD]] = add nsw i32 [[CALL2]], 2
-; CHECK-NEXT:    br i1 [[CMP]], label [[SELECT_UNFOLD]], label [[WHILE_COND]]
+; CHECK-NEXT:    [[COND_FR:%.*]] = freeze i1 [[CMP]]
+; CHECK-NEXT:    br i1 [[COND_FR]], label [[SELECT_UNFOLD]], label [[WHILE_COND]]
 ;
 entry:
   br label %while.cond
@@ -49,3 +50,47 @@ func1.exit:                  ; preds = %if.end.i, %while.cond, %while.cond
 
 declare i32 @func3(i32, i32, i32)
 
+declare void @sink(i32)
+
+; Check that unfoldSelectInstr freezes a possibly-undef-or-poison condition
+; before using it as a branch input. A select with an undef or poison condition
+; does not immediately cause UB, but branching on undef or poison does.
+define void @unfold_select_poison_cond(i1 %c, i1 %maybe_poison) {
+; CHECK-LABEL: define void @unfold_select_poison_cond(
+; CHECK-SAME: i1 [[C:%.*]], i1 [[MAYBE_POISON:%.*]]) {
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    br i1 [[C]], label %[[PRED1:.*]], label %[[IF_ELSE:.*]]
+; CHECK:       [[PRED1]]:
+; CHECK-NEXT:    [[COND_FR:%.*]] = freeze i1 [[MAYBE_POISON]]
+; CHECK-NEXT:    br i1 [[COND_FR]], label %[[IF_THEN:.*]], label %[[IF_ELSE]]
+; CHECK:       [[IF_THEN]]:
+; CHECK-NEXT:    call void @sink(i32 1)
+; CHECK-NEXT:    ret void
+; CHECK:       [[IF_ELSE]]:
+; CHECK-NEXT:    call void @sink(i32 2)
+; CHECK-NEXT:    ret void
+;
+entry:
+  br i1 %c, label %pred1, label %pred2
+
+pred1:
+  %s = select i1 %maybe_poison, i32 0, i32 10
+  br label %merge
+
+pred2:
+  br label %merge
+
+merge:
+  %p = phi i32 [ %s, %pred1 ], [ 5, %pred2 ]
+  %cmp = icmp eq i32 %p, 0
+  %fcmp = freeze i1 %cmp
+  br i1 %fcmp, label %if_then, label %if_else
+
+if_then:
+  call void @sink(i32 1)
+  ret void
+
+if_else:
+  call void @sink(i32 2)
+  ret void
+}



More information about the llvm-commits mailing list