[llvm] 4d093aa - [llubi] Check undefined bits when !noundef is set (#195642)

via llvm-commits llvm-commits at lists.llvm.org
Mon May 4 06:07:10 PDT 2026


Author: Yingwei Zheng
Date: 2026-05-04T21:07:05+08:00
New Revision: 4d093aa775638fd2bdb9fa3e4723aee84ec9a9e7

URL: https://github.com/llvm/llvm-project/commit/4d093aa775638fd2bdb9fa3e4723aee84ec9a9e7
DIFF: https://github.com/llvm/llvm-project/commit/4d093aa775638fd2bdb9fa3e4723aee84ec9a9e7.diff

LOG: [llubi] Check undefined bits when !noundef is set (#195642)

Address one of the todos in
https://github.com/llvm/llvm-project/pull/195339. Currently it just
checks the loaded value after applying poison-generating metadata. This
patch also takes uninitialized bits in the memory into account.

Added: 
    llvm/test/tools/llubi/load_noundef_ub_poison.ll
    llvm/test/tools/llubi/load_noundef_ub_poison_padding.ll
    llvm/test/tools/llubi/load_noundef_ub_undef.ll

Modified: 
    llvm/test/tools/llubi/loadstore_be.ll
    llvm/test/tools/llubi/loadstore_le.ll
    llvm/tools/llubi/lib/Context.cpp
    llvm/tools/llubi/lib/Context.h
    llvm/tools/llubi/lib/ExecutorBase.cpp
    llvm/tools/llubi/lib/ExecutorBase.h
    llvm/tools/llubi/lib/Interpreter.cpp

Removed: 
    


################################################################################
diff  --git a/llvm/test/tools/llubi/load_noundef_ub_poison.ll b/llvm/test/tools/llubi/load_noundef_ub_poison.ll
new file mode 100644
index 0000000000000..e8b6dafd3bbb2
--- /dev/null
+++ b/llvm/test/tools/llubi/load_noundef_ub_poison.ll
@@ -0,0 +1,16 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
+
+define void @main() {
+  %p = alloca i32
+  store i32 poison, ptr %p
+  %res = load i32, ptr %p, !noundef !{}
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %p = alloca i32, align 4 => ptr 0x8 [p]
+; CHECK-NEXT:   store i32 poison, ptr %p, align 4
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %res = load i32, ptr %p, align 4, !noundef !0 at @main
+; CHECK-NEXT: Immediate UB detected: The value loaded contains undefined bits.
+; CHECK-NEXT: error: Execution of function 'main' failed.

diff  --git a/llvm/test/tools/llubi/load_noundef_ub_poison_padding.ll b/llvm/test/tools/llubi/load_noundef_ub_poison_padding.ll
new file mode 100644
index 0000000000000..b57c8332a5bc9
--- /dev/null
+++ b/llvm/test/tools/llubi/load_noundef_ub_poison_padding.ll
@@ -0,0 +1,16 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
+
+define void @main() {
+  %p = alloca i8
+  store <2 x i4> <i4 1, i4 poison>, ptr %p
+  %res = load i4, ptr %p, !noundef !{}
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %p = alloca i8, align 1 => ptr 0x8 [p]
+; CHECK-NEXT:   store <2 x i4> <i4 1, i4 poison>, ptr %p, align 1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %res = load i4, ptr %p, align 1, !noundef !0 at @main
+; CHECK-NEXT: Immediate UB detected: The value loaded contains undefined bits.
+; CHECK-NEXT: error: Execution of function 'main' failed.

diff  --git a/llvm/test/tools/llubi/load_noundef_ub_undef.ll b/llvm/test/tools/llubi/load_noundef_ub_undef.ll
new file mode 100644
index 0000000000000..24220bdcd36bf
--- /dev/null
+++ b/llvm/test/tools/llubi/load_noundef_ub_undef.ll
@@ -0,0 +1,14 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
+
+define void @main() {
+  %p = alloca i32
+  %res = load i32, ptr %p, !noundef !{}
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %p = alloca i32, align 4 => ptr 0x8 [p]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %res = load i32, ptr %p, align 4, !noundef !0 at @main
+; CHECK-NEXT: Immediate UB detected: The value loaded contains undefined bits.
+; CHECK-NEXT: error: Execution of function 'main' failed.

diff  --git a/llvm/test/tools/llubi/loadstore_be.ll b/llvm/test/tools/llubi/loadstore_be.ll
index 34da612fdb581..2519a2f692a1a 100644
--- a/llvm/test/tools/llubi/loadstore_be.ll
+++ b/llvm/test/tools/llubi/loadstore_be.ll
@@ -107,6 +107,10 @@ define void @main() {
   %load_int_non_zero_padding = load i33, ptr %alloc_padding_vec
   %load_vec_non_zero_padding = load <3 x i11>, ptr %alloc_padding_vec
 
+  %alloc_struct_padding = alloca {i8, i32}
+  store {i8, i32} zeroinitializer, ptr %alloc_struct_padding
+  %load_struct_noundef = load {i8, i32}, ptr %alloc_struct_padding, !noundef !{}
+
   ret void
 }
 ; CHECK: Entering function: main
@@ -186,5 +190,8 @@ define void @main() {
 ; CHECK-NEXT:   %load_vec = load <6 x i5>, ptr %alloc_padding_vec, align 4 => { i5 0, i5 0, i5 0, i5 0, i5 0, i5 0 }
 ; CHECK-NEXT:   %load_int_non_zero_padding = load i33, ptr %alloc_padding_vec, align 8 => i33 255
 ; CHECK-NEXT:   %load_vec_non_zero_padding = load <3 x i11>, ptr %alloc_padding_vec, align 8 => { i11 0, i11 0, i11 255 }
+; CHECK-NEXT:   %alloc_struct_padding = alloca { i8, i32 }, align 8 => ptr 0x88 [alloc_struct_padding]
+; CHECK-NEXT:   store { i8, i32 } zeroinitializer, ptr %alloc_struct_padding, align 4
+; CHECK-NEXT:   %load_struct_noundef = load { i8, i32 }, ptr %alloc_struct_padding, align 4, !noundef !0 => { i8 0, i32 0 }
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: main

diff  --git a/llvm/test/tools/llubi/loadstore_le.ll b/llvm/test/tools/llubi/loadstore_le.ll
index bdd82ddabbfec..84c5246d799f6 100644
--- a/llvm/test/tools/llubi/loadstore_le.ll
+++ b/llvm/test/tools/llubi/loadstore_le.ll
@@ -108,6 +108,10 @@ define void @main() {
   %load_int_non_zero_padding = load i33, ptr %alloc_padding_vec
   %load_vec_non_zero_padding = load <3 x i11>, ptr %alloc_padding_vec
 
+  %alloc_struct_padding = alloca {i8, i32}
+  store {i8, i32} zeroinitializer, ptr %alloc_struct_padding
+  %load_struct_noundef = load {i8, i32}, ptr %alloc_struct_padding, !noundef !{}
+
   ret void
 }
 ; CHECK: Entering function: main
@@ -188,5 +192,8 @@ define void @main() {
 ; CHECK-NEXT:   %load_vec = load <6 x i5>, ptr %alloc_padding_vec, align 4 => { i5 0, i5 0, i5 0, i5 0, i5 0, i5 0 }
 ; CHECK-NEXT:   %load_int_non_zero_padding = load i33, ptr %alloc_padding_vec, align 8 => poison
 ; CHECK-NEXT:   %load_vec_non_zero_padding = load <3 x i11>, ptr %alloc_padding_vec, align 8 => { poison, poison, poison }
+; CHECK-NEXT:   %alloc_struct_padding = alloca { i8, i32 }, align 8 => ptr 0x88 [alloc_struct_padding]
+; CHECK-NEXT:   store { i8, i32 } zeroinitializer, ptr %alloc_struct_padding, align 4
+; CHECK-NEXT:   %load_struct_noundef = load { i8, i32 }, ptr %alloc_struct_padding, align 4, !noundef !0 => { i8 0, i32 0 }
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: main

diff  --git a/llvm/tools/llubi/lib/Context.cpp b/llvm/tools/llubi/lib/Context.cpp
index e591e9acc181e..5b81b068460de 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -111,7 +111,8 @@ const AnyValue &Context::getConstantValue(Constant *C) {
 }
 
 AnyValue Context::fromBytes(ConstBytesView Bytes, Type *Ty,
-                            uint32_t OffsetInBits, bool CheckPaddingBits) {
+                            uint32_t OffsetInBits, bool CheckPaddingBits,
+                            bool *ContainsUndefinedBits) {
   uint32_t NumBits = DL.getTypeSizeInBits(Ty).getFixedValue();
   uint32_t NewOffsetInBits = OffsetInBits + NumBits;
   if (CheckPaddingBits)
@@ -136,17 +137,23 @@ AnyValue Context::fromBytes(ConstBytesView Bytes, Type *Ty,
     uint32_t Mask = (1U << NumBitsInByte) - 1;
     // If any of the bits in the byte is poison, the whole value is poison.
     if (~LogicalByte.ConcreteMask & ~LogicalByte.Value & Mask) {
+      if (ContainsUndefinedBits)
+        *ContainsUndefinedBits = true;
       OffsetInBits = NewOffsetInBits;
       return AnyValue::poison();
     }
     uint8_t RandomBits = 0;
-    if (UndefBehavior == UndefValueBehavior::NonDeterministic &&
-        (~LogicalByte.ConcreteMask & Mask)) {
+    if (~LogicalByte.ConcreteMask & Mask) {
       // This byte contains undef bits.
-      // We don't use std::uniform_int_distribution here because it produces
-      // 
diff erent results across 
diff erent library implementations. Instead,
-      // we directly use the low bits from Rng.
-      RandomBits = static_cast<uint8_t>(Rng());
+      if (ContainsUndefinedBits)
+        *ContainsUndefinedBits = true;
+
+      if (UndefBehavior == UndefValueBehavior::NonDeterministic) {
+        // We don't use std::uniform_int_distribution here because it produces
+        // 
diff erent results across 
diff erent library implementations. Instead,
+        // we directly use the low bits from Rng.
+        RandomBits = static_cast<uint8_t>(Rng());
+      }
     }
     uint8_t ActualBits = ((LogicalByte.Value & LogicalByte.ConcreteMask) |
                           (RandomBits & ~LogicalByte.ConcreteMask)) &
@@ -159,8 +166,11 @@ AnyValue Context::fromBytes(ConstBytesView Bytes, Type *Ty,
 
   // Padding bits for non-byte-sized scalar types must be zero.
   if (NeedsPadding) {
-    if (!Bits.isIntN(NumBits))
+    if (!Bits.isIntN(NumBits)) {
+      if (ContainsUndefinedBits)
+        *ContainsUndefinedBits = true;
       return AnyValue::poison();
+    }
     Bits = Bits.trunc(NumBits);
   }
 
@@ -173,12 +183,13 @@ AnyValue Context::fromBytes(ConstBytesView Bytes, Type *Ty,
   return Pointer(Bits);
 }
 
-AnyValue Context::fromBytes(ArrayRef<Byte> Bytes, Type *Ty) {
+AnyValue Context::fromBytes(ArrayRef<Byte> Bytes, Type *Ty,
+                            bool *ContainsUndefinedBits) {
   assert(Bytes.size() == getEffectiveTypeStoreSize(Ty) &&
          "Invalid byte array size for the type");
   if (Ty->isIntegerTy() || Ty->isFloatingPointTy() || Ty->isPointerTy())
     return fromBytes(ConstBytesView(Bytes, DL), Ty, /*OffsetInBits=*/0,
-                     /*CheckPaddingBits=*/true);
+                     /*CheckPaddingBits=*/true, ContainsUndefinedBits);
 
   if (auto *VecTy = dyn_cast<VectorType>(Ty)) {
     Type *ElemTy = VecTy->getElementType();
@@ -192,8 +203,11 @@ AnyValue Context::fromBytes(ArrayRef<Byte> Bytes, Type *Ty) {
       const Byte &PaddingByte = View[Bytes.size() - 1];
       uint32_t Mask = (~0U << (VecBits % 8)) & 255U;
       // Make sure all high padding bits are zero.
-      if ((PaddingByte.ConcreteMask & ~PaddingByte.Value & Mask) != Mask)
+      if ((PaddingByte.ConcreteMask & ~PaddingByte.Value & Mask) != Mask) {
+        if (ContainsUndefinedBits)
+          *ContainsUndefinedBits = true;
         return AnyValue::getPoisonValue(*this, Ty);
+      }
     }
 
     std::vector<AnyValue> ValVec;
@@ -202,11 +216,11 @@ AnyValue Context::fromBytes(ArrayRef<Byte> Bytes, Type *Ty) {
     // the integer, and for big endian element zero is put in the most
     // significant bits.
     for (uint32_t I = 0; I != NumElements; ++I)
-      ValVec.push_back(fromBytes(View, ElemTy,
-                                 DL.isLittleEndian()
-                                     ? I * ElemBits
-                                     : VecBits - ElemBits - I * ElemBits,
-                                 /*CheckPaddingBits=*/false));
+      ValVec.push_back(
+          fromBytes(View, ElemTy,
+                    DL.isLittleEndian() ? I * ElemBits
+                                        : VecBits - ElemBits - I * ElemBits,
+                    /*CheckPaddingBits=*/false, ContainsUndefinedBits));
     return AnyValue(std::move(ValVec));
   }
   if (auto *ArrTy = dyn_cast<ArrayType>(Ty)) {
@@ -217,7 +231,8 @@ AnyValue Context::fromBytes(ArrayRef<Byte> Bytes, Type *Ty) {
     std::vector<AnyValue> ValVec;
     ValVec.reserve(NumElements);
     for (uint32_t I = 0; I != NumElements; ++I)
-      ValVec.push_back(fromBytes(Bytes.slice(I * Stride, StoreSize), ElemTy));
+      ValVec.push_back(fromBytes(Bytes.slice(I * Stride, StoreSize), ElemTy,
+                                 ContainsUndefinedBits));
     return AnyValue(std::move(ValVec));
   }
   if (auto *StructTy = dyn_cast<StructType>(Ty)) {
@@ -230,7 +245,7 @@ AnyValue Context::fromBytes(ArrayRef<Byte> Bytes, Type *Ty) {
       ValVec.push_back(fromBytes(
           Bytes.slice(getEffectiveTypeSize(Layout->getElementOffset(I)),
                       getEffectiveTypeStoreSize(ElemTy)),
-          ElemTy));
+          ElemTy, ContainsUndefinedBits));
     }
     return AnyValue(std::move(ValVec));
   }
@@ -358,9 +373,11 @@ void Context::toBytes(const AnyValue &Val, Type *Ty,
   llvm_unreachable("Unsupported first class type.");
 }
 
-AnyValue Context::load(MemoryObject &MO, uint64_t Offset, Type *ValTy) {
+AnyValue Context::load(MemoryObject &MO, uint64_t Offset, Type *ValTy,
+                       bool *ContainsUndefinedBits) {
   return fromBytes(
-      MO.getBytes().slice(Offset, getEffectiveTypeStoreSize(ValTy)), ValTy);
+      MO.getBytes().slice(Offset, getEffectiveTypeStoreSize(ValTy)), ValTy,
+      ContainsUndefinedBits);
 }
 
 void Context::store(MemoryObject &MO, uint64_t Offset, const AnyValue &Val,

diff  --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index 9e128076ca17e..aa9d61cc6077e 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -211,7 +211,7 @@ class Context {
   // the provenance.
   std::map<uint64_t, IntrusiveRefCntPtr<MemoryObject>> MemoryObjects;
   AnyValue fromBytes(ConstBytesView Bytes, Type *Ty, uint32_t OffsetInBits,
-                     bool CheckPaddingBits);
+                     bool CheckPaddingBits, bool *ContainsUndefinedBits);
   void toBytes(const AnyValue &Val, Type *Ty, uint32_t OffsetInBits,
                MutableBytesView Bytes, bool PaddingBits);
 
@@ -279,11 +279,15 @@ class Context {
   Pointer deriveFromMemoryObject(IntrusiveRefCntPtr<MemoryObject> Obj);
   /// Convert byte sequence to a value of the given type. Uninitialized bits are
   /// flushed according to the options.
-  AnyValue fromBytes(ArrayRef<Byte> Bytes, Type *Ty);
+  /// If \p ContainsUndefinedBits is provided, it will be set to true when there
+  /// are poison or undef bits in the value (i.e., padding bits are ignored).
+  AnyValue fromBytes(ArrayRef<Byte> Bytes, Type *Ty,
+                     bool *ContainsUndefinedBits = nullptr);
   /// Convert a value to byte sequence. Padding bits are set to zero.
   void toBytes(const AnyValue &Val, Type *Ty, MutableArrayRef<Byte> Bytes);
   /// Direct memory load without checks.
-  AnyValue load(MemoryObject &MO, uint64_t Offset, Type *ValTy);
+  AnyValue load(MemoryObject &MO, uint64_t Offset, Type *ValTy,
+                bool *ContainsUndefinedBits = nullptr);
   /// Direct memory store without checks.
   void store(MemoryObject &MO, uint64_t Offset, const AnyValue &Val,
              Type *ValTy);

diff  --git a/llvm/tools/llubi/lib/ExecutorBase.cpp b/llvm/tools/llubi/lib/ExecutorBase.cpp
index 233497e041b00..5c1479d277bf9 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.cpp
+++ b/llvm/tools/llubi/lib/ExecutorBase.cpp
@@ -100,7 +100,8 @@ std::optional<uint64_t> ExecutorBase::verifyMemAccess(const MemoryObject &MO,
   return Offset.getZExtValue();
 }
 
-AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy) {
+AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
+                            bool NoUndef) {
   if (Ptr.isPoison()) {
     reportImmediateUB() << "Invalid memory access with a poison pointer.";
     return AnyValue::getPoisonValue(Ctx, ValTy);
@@ -121,7 +122,12 @@ AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy) {
     if (MO->getState() == MemoryObjectState::Dead)
       return AnyValue::getPoisonValue(Ctx, ValTy);
 
-    return Ctx.load(*MO, *Offset, ValTy);
+    bool ContainsUndefinedBits = false;
+    AnyValue Res = Ctx.load(*MO, *Offset, ValTy,
+                            NoUndef ? &ContainsUndefinedBits : nullptr);
+    if (NoUndef && ContainsUndefinedBits)
+      reportImmediateUB() << "The value loaded contains undefined bits.";
+    return Res;
   }
   return AnyValue::getPoisonValue(Ctx, ValTy);
 }

diff  --git a/llvm/tools/llubi/lib/ExecutorBase.h b/llvm/tools/llubi/lib/ExecutorBase.h
index 64933929fdc35..d07b4dae1792e 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.h
+++ b/llvm/tools/llubi/lib/ExecutorBase.h
@@ -106,7 +106,8 @@ class ExecutorBase {
                                           uint64_t AccessSize, Align Alignment,
                                           bool IsStore);
 
-  AnyValue load(const AnyValue &Ptr, Align Alignment, Type *ValTy);
+  AnyValue load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
+                bool NoUndef);
   void store(const AnyValue &Ptr, Align Alignment, const AnyValue &Val,
              Type *ValTy);
 

diff  --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index ec6ee07a35471..f50f074d9d457 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -1691,10 +1691,9 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitLoadInst(LoadInst &LI) {
-    auto RetVal =
-        load(getValue(LI.getPointerOperand()), LI.getAlign(), LI.getType());
+    auto RetVal = load(getValue(LI.getPointerOperand()), LI.getAlign(),
+                       LI.getType(), LI.hasMetadata(LLVMContext::MD_noundef));
     // TODO: track volatile loads
-    // TODO: Check undef bits when !noundef is set.
     handleMetadata(LI.getType(), RetVal, LI);
     setResult(LI, std::move(RetVal));
   }


        


More information about the llvm-commits mailing list