[llvm] 2060561 - Reapply "[msan] Switch switch() from strict handling to (icmp eq)-style handling" (#180636) (#181112)
via llvm-commits
llvm-commits at lists.llvm.org
Fri Feb 13 22:15:10 PST 2026
Author: Thurston Dang
Date: 2026-02-14T17:15:05+11:00
New Revision: 20605617ac69de3dad49139a3de34b4dd2609607
URL: https://github.com/llvm/llvm-project/commit/20605617ac69de3dad49139a3de34b4dd2609607
DIFF: https://github.com/llvm/llvm-project/commit/20605617ac69de3dad49139a3de34b4dd2609607.diff
LOG: Reapply "[msan] Switch switch() from strict handling to (icmp eq)-style handling" (#180636) (#181112)
This reverts https://github.com/llvm/llvm-project/pull/180636 i.e.,
relands https://github.com/llvm/llvm-project/pull/179851.
It was originally reverted because of buildbot failures. When compiling
switch statements with many cases (e.g., AMDGPUGenMCCodeEmitter.inc has
>30,000 cases), MSan's instrumentation created an extremely long chained
expression for the shadow computation. Although that was legal LLVM IR,
it caused the subsequent JumpThreadingPass to have a stack overflow.
This reland avoids the issue by limiting the number of cases considered
(`-msan-switch-precision`), with the tradeoff of niche false negatives
(only in the case where the condition is partly uninitialized and the
first x cases still have a defined comparison, but a case # > x does not
have a fully-defined comparison).
This reland also adds some TODOs for possible improvements.
----
Original commit message:
Currently, the SwitchInst:
```
switch i32 %Val, label %else [ i32 0, label %A
i32 1, label %B
i32 2, label %C ]
```
is strictly handled i.e., MSan will check that %Val is fully
initialized. This is appropriate nearly all the time.
However, sometimes the compiler may convert (icmp + br) into a switch
statement. (icmp + br) has different semantics: MSan allows icmp eq/ne
with partly initialized inputs to still result in a fully initialized
output, if there exists a bit that is initialized in both inputs with a
differing value e.g., suppose:
```
%A = 00000000 00001010
%B = 00000000 00000110
%C = 00000000 00000011
%Val = 00000001 ???????? (where ? denotes an uninitialized bit)
```
Even though %Val has uninitialized bits, the initialized '1' bit
immediately to the left, compared to the corresponding initialized '0'
bit in %A/%B/%C suffices to prove that %Val does not match any of those
cases. This is similar to a real-world case with std::optional (where
the has_value bit may be initialized but the value is not).
This patch adds this relaxed icmp logic to the switch instrumentation as
well, to make MSan's behavior equivalent under optimization.
Note that this edge case only applies if the switch input value
definitively does not match *any* of the cases (matching any of the
cases requires an exact, fully initialized match). If it is uncertain
whether the switch input value could, depending on the uninitialized
bits, match one of the cases or not, MSan will report
use-of-uninitialized memory.
Added:
Modified:
llvm/lib/Transforms/Instrumentation/MemorySanitizer.cpp
llvm/test/Instrumentation/MemorySanitizer/switch-icmp.ll
Removed:
################################################################################
diff --git a/llvm/lib/Transforms/Instrumentation/MemorySanitizer.cpp b/llvm/lib/Transforms/Instrumentation/MemorySanitizer.cpp
index 5e766d8e0ff8c..2c0232a266d04 100644
--- a/llvm/lib/Transforms/Instrumentation/MemorySanitizer.cpp
+++ b/llvm/lib/Transforms/Instrumentation/MemorySanitizer.cpp
@@ -299,6 +299,15 @@ static cl::opt<bool>
cl::desc("exact handling of relational integer ICmp"),
cl::Hidden, cl::init(true));
+static cl::opt<int> ClSwitchPrecision(
+ "msan-switch-precision",
+ cl::desc("Controls the number of cases considered by MSan for LLVM switch "
+ "instructions. 0 means no UUMs detected. Higher values lead to "
+ "fewer false negatives but may impact compiler and/or "
+ "application performance. N.B. LLVM switch instructions do not "
+ "correspond exactly to C++ switch statements."),
+ cl::Hidden, cl::init(99));
+
static cl::opt<bool> ClHandleLifetimeIntrinsics(
"msan-handle-lifetime-intrinsics",
cl::desc(
@@ -2460,6 +2469,64 @@ struct MemorySanitizerVisitor : public InstVisitor<MemorySanitizerVisitor> {
return Si;
}
+ // Instrument:
+ // switch i32 %Val, label %else [ i32 0, label %A
+ // i32 1, label %B
+ // i32 2, label %C ]
+ //
+ // Typically, the switch input value (%Val) is fully initialized.
+ //
+ // Sometimes the compiler may convert (icmp + br) into a switch statement.
+ // MSan allows icmp eq/ne with partly initialized inputs to still result in a
+ // fully initialized output, if there exists a bit that is initialized in
+ // both inputs with a
diff ering value. For compatibility, we support this in
+ // the switch instrumentation as well. Note that this edge case only applies
+ // if the switch input value does not match *any* of the cases (matching any
+ // of the cases requires an exact, fully initialized match).
+ //
+ // ShadowCases = 0
+ // | propagateEqualityComparison(Val, 0)
+ // | propagateEqualityComparison(Val, 1)
+ // | propagateEqualityComparison(Val, 2))
+ void visitSwitchInst(SwitchInst &SI) {
+ IRBuilder<> IRB(&SI);
+
+ Value *Val = SI.getCondition();
+ Value *ShadowVal = getShadow(Val);
+ // TODO: add fast path - if the condition is fully initialized, we know
+ // there is no UUM, without needing to consider the case values below.
+
+ // Some code (e.g., AMDGPUGenMCCodeEmitter.inc) has tens of thousands of
+ // cases. This results in an extremely long chained expression for MSan's
+ // switch instrumentation, which can cause the JumpThreadingPass to have a
+ // stack overflow or excessive runtime. We limit the number of cases
+ // considered, with the tradeoff of niche false negatives.
+ // TODO: figure out a better solution.
+ int casesToConsider = ClSwitchPrecision;
+
+ Value *ShadowCases = nullptr;
+ for (auto Case : SI.cases()) {
+ if (casesToConsider <= 0)
+ break;
+
+ Value *Comparator = Case.getCaseValue();
+ // TODO: some simplification is possible when comparing multiple cases
+ // simultaneously.
+ Value *ComparisonShadow = propagateEqualityComparison(
+ IRB, Val, Comparator, ShadowVal, getShadow(Comparator));
+
+ if (ShadowCases)
+ ShadowCases = IRB.CreateOr(ShadowCases, ComparisonShadow);
+ else
+ ShadowCases = ComparisonShadow;
+
+ casesToConsider--;
+ }
+
+ if (ShadowCases)
+ insertCheckShadow(ShadowCases, getOrigin(Val), &SI);
+ }
+
// Vector manipulation.
void visitExtractElementInst(ExtractElementInst &I) {
insertCheckShadowOf(I.getOperand(1), &I);
diff --git a/llvm/test/Instrumentation/MemorySanitizer/switch-icmp.ll b/llvm/test/Instrumentation/MemorySanitizer/switch-icmp.ll
index 08c9a5a62febf..08e56516ff5b5 100644
--- a/llvm/test/Instrumentation/MemorySanitizer/switch-icmp.ll
+++ b/llvm/test/Instrumentation/MemorySanitizer/switch-icmp.ll
@@ -8,9 +8,10 @@
; uninitialized, if a bit is initialized in both inputs but has a
diff erent
; value.
;
-; TODO: since the compiler/optimizer may freely choose between (icmp eq + br)
-; vs. switch, MSan's switch instrumentation also needs to be able to
-; handle partly-uninitialized inputs.
+; If switch has a partly uninitialized input, but it is possible to rule out
+; matching any of the cases, it will use the default case instead of reporting
+; use-of-uninitialized memory. This is equivalent to if the switch was replaced
+; by a series of (icmp eq + br).
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"
@@ -20,8 +21,22 @@ define i64 @switch_test(i32 %wii) sanitize_memory {
; CHECK-SAME: i32 [[WII:%.*]]) #[[ATTR0:[0-9]+]] {
; CHECK-NEXT: [[TMP1:%.*]] = load i32, ptr @__msan_param_tls, align 8
; CHECK-NEXT: call void @llvm.donothing()
-; CHECK-NEXT: [[_MSCMP:%.*]] = icmp ne i32 [[TMP1]], 0
-; CHECK-NEXT: br i1 [[_MSCMP]], label %[[BB2:.*]], label %[[BB3:.*]], !prof [[PROF1:![0-9]+]]
+; CHECK-NEXT: [[TMP3:%.*]] = xor i32 [[WII]], 42
+; CHECK-NEXT: [[TMP4:%.*]] = or i32 [[TMP1]], 0
+; CHECK-NEXT: [[TMP5:%.*]] = icmp ne i32 [[TMP4]], 0
+; CHECK-NEXT: [[TMP6:%.*]] = xor i32 [[TMP4]], -1
+; CHECK-NEXT: [[TMP7:%.*]] = and i32 [[TMP6]], [[TMP3]]
+; CHECK-NEXT: [[TMP8:%.*]] = icmp eq i32 [[TMP7]], 0
+; CHECK-NEXT: [[_MSPROP_ICMP:%.*]] = and i1 [[TMP5]], [[TMP8]]
+; CHECK-NEXT: [[TMP9:%.*]] = xor i32 [[WII]], 43
+; CHECK-NEXT: [[TMP10:%.*]] = or i32 [[TMP1]], 0
+; CHECK-NEXT: [[TMP11:%.*]] = icmp ne i32 [[TMP10]], 0
+; CHECK-NEXT: [[TMP12:%.*]] = xor i32 [[TMP10]], -1
+; CHECK-NEXT: [[TMP13:%.*]] = and i32 [[TMP12]], [[TMP9]]
+; CHECK-NEXT: [[TMP14:%.*]] = icmp eq i32 [[TMP13]], 0
+; CHECK-NEXT: [[_MSPROP_ICMP1:%.*]] = and i1 [[TMP11]], [[TMP14]]
+; CHECK-NEXT: [[TMP15:%.*]] = or i1 [[_MSPROP_ICMP]], [[_MSPROP_ICMP1]]
+; CHECK-NEXT: br i1 [[TMP15]], label %[[BB2:.*]], label %[[BB3:.*]], !prof [[PROF1:![0-9]+]]
; CHECK: [[BB2]]:
; CHECK-NEXT: call void @__msan_warning_noreturn() #[[ATTR3:[0-9]+]]
; CHECK-NEXT: unreachable
More information about the llvm-commits
mailing list