[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