[llvm] [llubi] Improve diagnostics and add stacktrace (PR #195449)

Zhige Chen via llvm-commits llvm-commits at lists.llvm.org
Sat May 2 06:49:41 PDT 2026


https://github.com/nofe1248 updated https://github.com/llvm/llvm-project/pull/195449

>From c48efd6ecec381b235a6215411f9d9657d30284d Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhigec_cpp at outlook.com>
Date: Sat, 2 May 2026 21:15:41 +0800
Subject: [PATCH 1/3] [llubi] Improve diagnostics and add stacktrace

---
 llvm/test/tools/llubi/alloca_large_count.ll   |  4 +-
 llvm/test/tools/llubi/alloca_large_size.ll    |  2 +
 llvm/test/tools/llubi/alloca_poison_count.ll  |  2 +
 llvm/test/tools/llubi/alloca_size_overflow.ll |  4 +-
 llvm/test/tools/llubi/assume_false.ll         |  2 +
 llvm/test/tools/llubi/assume_poison.ll        |  2 +
 llvm/test/tools/llubi/br_poison.ll            |  2 +
 .../tools/llubi/call_mismatched_signature.ll  |  4 +-
 llvm/test/tools/llubi/call_poison.ll          |  2 +
 llvm/test/tools/llubi/divrem_ub1.ll           |  2 +
 llvm/test/tools/llubi/divrem_ub2.ll           |  2 +
 llvm/test/tools/llubi/divrem_ub3.ll           |  2 +
 llvm/test/tools/llubi/divrem_ub4.ll           |  2 +
 llvm/test/tools/llubi/indirectbr_invalid.ll   |  2 +
 llvm/test/tools/llubi/indirectbr_poison.ll    |  2 +
 llvm/test/tools/llubi/infinite_loop.ll        |  2 +
 llvm/test/tools/llubi/invoke_poison.ll        |  3 +
 .../tools/llubi/lib_cxx_memory_large_size.ll  |  2 +
 llvm/test/tools/llubi/lib_double_free.ll      |  4 +-
 .../tools/llubi/lib_free_nullary_pointer.ll   |  2 +
 .../test/tools/llubi/lib_free_out_of_bound.ll |  4 +-
 llvm/test/tools/llubi/lib_free_stack.ll       |  4 +-
 llvm/test/tools/llubi/lib_poison_argument.ll  |  4 +-
 .../llubi/lib_printf_not_enough_argument.ll   |  4 +-
 .../llubi/lib_printf_unknown_specifier.ll     |  4 +-
 .../tools/llubi/lib_read_nullary_string.ll    |  2 +
 llvm/test/tools/llubi/lib_uninit_string.ll    |  4 +-
 llvm/test/tools/llubi/loadstore_misaligned.ll |  2 +
 llvm/test/tools/llubi/loadstore_null.ll       |  2 +
 llvm/test/tools/llubi/loadstore_oob1.ll       |  4 +-
 llvm/test/tools/llubi/loadstore_poison.ll     |  2 +
 llvm/test/tools/llubi/loadstore_uaf.ll        |  4 +-
 llvm/test/tools/llubi/stack_overflow.ll       | 11 +++
 llvm/test/tools/llubi/store_dead.ll           |  4 +-
 llvm/test/tools/llubi/switch_poison.ll        |  3 +
 llvm/test/tools/llubi/unreachable.ll          |  2 +
 llvm/tools/llubi/lib/ExecutorBase.cpp         | 62 +++++++++++++---
 llvm/tools/llubi/lib/ExecutorBase.h           | 49 +++++++++++-
 llvm/tools/llubi/lib/Interpreter.cpp          | 74 ++++++++++---------
 llvm/tools/llubi/lib/Library.cpp              | 59 +++++++++------
 40 files changed, 271 insertions(+), 82 deletions(-)

diff --git a/llvm/test/tools/llubi/alloca_large_count.ll b/llvm/test/tools/llubi/alloca_large_count.ll
index 62772317ae561..3601f4aa689ca 100644
--- a/llvm/test/tools/llubi/alloca_large_count.ll
+++ b/llvm/test/tools/llubi/alloca_large_count.ll
@@ -6,5 +6,7 @@ define void @main() {
   ret void
 }
 ; CHECK: Entering function: main
-; CHECK-NEXT: Immediate UB detected: Alloca with large array size that overflows uint64_t.
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %alloc_dyn = alloca i32, i128 -1, align 4 at @main
+; CHECK-NEXT: Immediate UB detected: Alloca with large array size that overflows uint64_t. Size: -1
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/alloca_large_size.ll b/llvm/test/tools/llubi/alloca_large_size.ll
index a9773d3bcb184..478dc88c36206 100644
--- a/llvm/test/tools/llubi/alloca_large_size.ll
+++ b/llvm/test/tools/llubi/alloca_large_size.ll
@@ -6,5 +6,7 @@ define void @main() {
   ret void
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %alloc_dyn = alloca i32, i32 100, align 4 at @main
 ; CHECK-NEXT: Error: Insufficient stack space.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/alloca_poison_count.ll b/llvm/test/tools/llubi/alloca_poison_count.ll
index c8c97c4517bfc..4b092bb41033f 100644
--- a/llvm/test/tools/llubi/alloca_poison_count.ll
+++ b/llvm/test/tools/llubi/alloca_poison_count.ll
@@ -6,5 +6,7 @@ define void @main() {
   ret void
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %alloc_dyn = alloca i32, i32 poison, align 4 at @main
 ; CHECK-NEXT: Immediate UB detected: Alloca with poison array size.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/alloca_size_overflow.ll b/llvm/test/tools/llubi/alloca_size_overflow.ll
index 258f94d57d90f..f70e72d1c47c9 100644
--- a/llvm/test/tools/llubi/alloca_size_overflow.ll
+++ b/llvm/test/tools/llubi/alloca_size_overflow.ll
@@ -6,5 +6,7 @@ define void @main() {
   ret void
 }
 ; CHECK: Entering function: main
-; CHECK-NEXT: Immediate UB detected: Alloca with allocation size that overflows uint64_t.
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %alloc_dyn = alloca i32, i64 -1, align 4 at @main
+; CHECK-NEXT: Immediate UB detected: Alloca with allocation size that overflows uint64_t. Size: -1
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/assume_false.ll b/llvm/test/tools/llubi/assume_false.ll
index d621db1db97be..a8bb04c259ec6 100644
--- a/llvm/test/tools/llubi/assume_false.ll
+++ b/llvm/test/tools/llubi/assume_false.ll
@@ -6,5 +6,7 @@ define void @main() {
   ret void
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @llvm.assume(i1 false) at @main
 ; CHECK-NEXT: Immediate UB detected: Assume on false or poison condition.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/assume_poison.ll b/llvm/test/tools/llubi/assume_poison.ll
index c178c19f2d221..a33bf9224a497 100644
--- a/llvm/test/tools/llubi/assume_poison.ll
+++ b/llvm/test/tools/llubi/assume_poison.ll
@@ -6,5 +6,7 @@ define void @main() {
   ret void
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @llvm.assume(i1 poison) at @main
 ; CHECK-NEXT: Immediate UB detected: Assume on false or poison condition.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/br_poison.ll b/llvm/test/tools/llubi/br_poison.ll
index dcad8d0584767..c6be9f8c09637 100644
--- a/llvm/test/tools/llubi/br_poison.ll
+++ b/llvm/test/tools/llubi/br_poison.ll
@@ -9,5 +9,7 @@ exit:
   ret void
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   br i1 poison, label %exit, label %exit at @main
 ; CHECK-NEXT: Immediate UB detected: Branch on poison condition.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/call_mismatched_signature.ll b/llvm/test/tools/llubi/call_mismatched_signature.ll
index 9c65dd231826c..50227d310fa01 100644
--- a/llvm/test/tools/llubi/call_mismatched_signature.ll
+++ b/llvm/test/tools/llubi/call_mismatched_signature.ll
@@ -10,5 +10,7 @@ define void @main() {
   ret void
 }
 ; CHECK: Entering function: main
-; CHECK-NEXT: Immediate UB detected: Indirect call through a function pointer with mismatched signature.
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @foo() at @main
+; CHECK-NEXT: Immediate UB detected: Indirect call through a function pointer with mismatched signature. Expected: void (), Actual: i32 ()
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/call_poison.ll b/llvm/test/tools/llubi/call_poison.ll
index a9dec9e0603f1..01c76e189bdd0 100644
--- a/llvm/test/tools/llubi/call_poison.ll
+++ b/llvm/test/tools/llubi/call_poison.ll
@@ -7,5 +7,7 @@ entry:
   ret void
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void poison() at @main
 ; CHECK-NEXT: Immediate UB detected: Indirect call through poison function pointer.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/divrem_ub1.ll b/llvm/test/tools/llubi/divrem_ub1.ll
index 00b6dfeca43f5..7923ad4e20967 100644
--- a/llvm/test/tools/llubi/divrem_ub1.ll
+++ b/llvm/test/tools/llubi/divrem_ub1.ll
@@ -8,5 +8,7 @@ define void @main() {
   ret void
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0 %res = {{sdiv|udiv|srem|urem}} <2 x i32> splat (i32 10), zeroinitializer at @main
 ; CHECK-NEXT: Immediate UB detected: Division by zero.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/divrem_ub2.ll b/llvm/test/tools/llubi/divrem_ub2.ll
index 03a941da1e7a0..b4bdbac3f2c2e 100644
--- a/llvm/test/tools/llubi/divrem_ub2.ll
+++ b/llvm/test/tools/llubi/divrem_ub2.ll
@@ -8,5 +8,7 @@ define void @main() {
   ret void
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0 %res = {{sdiv|udiv|srem|urem}} i32 10, poison at @main
 ; CHECK-NEXT: Immediate UB detected: Division by zero (refine RHS to 0).
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/divrem_ub3.ll b/llvm/test/tools/llubi/divrem_ub3.ll
index 7ab668d6b9c8a..00e91bffd2f63 100644
--- a/llvm/test/tools/llubi/divrem_ub3.ll
+++ b/llvm/test/tools/llubi/divrem_ub3.ll
@@ -6,5 +6,7 @@ define void @main() {
   ret void
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0 %res = {{sdiv|udiv|srem|urem}} i8 -128, -1 at @main
 ; CHECK-NEXT: Immediate UB detected: Signed division overflow.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/divrem_ub4.ll b/llvm/test/tools/llubi/divrem_ub4.ll
index 90a7d7c025f23..cdb673d55cd55 100644
--- a/llvm/test/tools/llubi/divrem_ub4.ll
+++ b/llvm/test/tools/llubi/divrem_ub4.ll
@@ -6,5 +6,7 @@ define void @main() {
   ret void
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0 %res = {{sdiv|udiv|srem|urem}} i8 poison, -1 at @main
 ; CHECK-NEXT: Immediate UB detected: Signed division overflow (refine LHS to INT_MIN).
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/indirectbr_invalid.ll b/llvm/test/tools/llubi/indirectbr_invalid.ll
index 9ffb26b0aebbb..2dfb99db36f09 100644
--- a/llvm/test/tools/llubi/indirectbr_invalid.ll
+++ b/llvm/test/tools/llubi/indirectbr_invalid.ll
@@ -12,5 +12,7 @@ exit:
   ret void
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   indirectbr ptr blockaddress(@main, %bb2), [label %exit] at @main
 ; CHECK-NEXT: Immediate UB detected: Indirect branch on unlisted target BB.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/indirectbr_poison.ll b/llvm/test/tools/llubi/indirectbr_poison.ll
index 076aa79c50a52..d2c5fa266a662 100644
--- a/llvm/test/tools/llubi/indirectbr_poison.ll
+++ b/llvm/test/tools/llubi/indirectbr_poison.ll
@@ -9,5 +9,7 @@ exit:
   ret void
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   indirectbr ptr poison, [label %exit] at @main
 ; CHECK-NEXT: Immediate UB detected: Indirect branch on poison.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/infinite_loop.ll b/llvm/test/tools/llubi/infinite_loop.ll
index bc0fda540b758..01f38589c2d07 100644
--- a/llvm/test/tools/llubi/infinite_loop.ll
+++ b/llvm/test/tools/llubi/infinite_loop.ll
@@ -19,5 +19,7 @@ loop:
 ; CHECK-NEXT:   br label %loop jump to %loop
 ; CHECK-NEXT:   br label %loop jump to %loop
 ; CHECK-NEXT:   br label %loop jump to %loop
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   br label %loop at @main
 ; CHECK-NEXT: Error: Exceeded maximum number of execution steps.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/invoke_poison.ll b/llvm/test/tools/llubi/invoke_poison.ll
index 709f3e96b8a28..21927d87f332a 100644
--- a/llvm/test/tools/llubi/invoke_poison.ll
+++ b/llvm/test/tools/llubi/invoke_poison.ll
@@ -13,5 +13,8 @@ exit:
   ret void
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   invoke void poison()
+; CHECK-NEXT:           to label %exit unwind label %cleanup at @main
 ; CHECK-NEXT: Immediate UB detected: Indirect call through poison function pointer.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/lib_cxx_memory_large_size.ll b/llvm/test/tools/llubi/lib_cxx_memory_large_size.ll
index 207bb469c9199..a8e7c58051e5a 100644
--- a/llvm/test/tools/llubi/lib_cxx_memory_large_size.ll
+++ b/llvm/test/tools/llubi/lib_cxx_memory_large_size.ll
@@ -11,5 +11,7 @@ define i32 @main() {
 }
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %ptr_1 = call ptr @_Znwm(i64 50) => ptr 0x10 [ptr_1]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %ptr_2 = call ptr @_Znwm(i64 100) at @main
 ; CHECK-NEXT: Error: Insufficient heap space.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/lib_double_free.ll b/llvm/test/tools/llubi/lib_double_free.ll
index db0a684e74193..19408d796b467 100644
--- a/llvm/test/tools/llubi/lib_double_free.ll
+++ b/llvm/test/tools/llubi/lib_double_free.ll
@@ -17,5 +17,7 @@ entry:
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %ptr = call ptr @malloc(i64 4) => ptr 0x10 [ptr]
 ; CHECK-NEXT:   call void @free(ptr %ptr)
-; CHECK-NEXT: Immediate UB detected: double-freeing a memory object.
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @free(ptr %ptr) at @main
+; CHECK-NEXT: Immediate UB detected: double-freeing a memory object allocated at 0x10.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/lib_free_nullary_pointer.ll b/llvm/test/tools/llubi/lib_free_nullary_pointer.ll
index b487a7b335f60..251608a547ed9 100644
--- a/llvm/test/tools/llubi/lib_free_nullary_pointer.ll
+++ b/llvm/test/tools/llubi/lib_free_nullary_pointer.ll
@@ -11,5 +11,7 @@ define i32 @main() {
 }
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %p = getelementptr i8, ptr null, i64 42 => ptr 0x2A [dangling]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @free(ptr %p) at @main
 ; CHECK-NEXT: Immediate UB detected: freeing a pointer with nullary provenance.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/lib_free_out_of_bound.ll b/llvm/test/tools/llubi/lib_free_out_of_bound.ll
index 827df76ce3562..5ab7fa2452103 100644
--- a/llvm/test/tools/llubi/lib_free_out_of_bound.ll
+++ b/llvm/test/tools/llubi/lib_free_out_of_bound.ll
@@ -14,5 +14,7 @@ define i32 @main() {
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %p = call ptr @malloc(i64 4) => ptr 0x10 [p]
 ; CHECK-NEXT:   %p_oob = getelementptr i8, ptr %p, i64 8 => ptr 0x18 [p + 8]
-; CHECK-NEXT: Immediate UB detected: freeing a pointer that does not point to the start of an allocation.
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @free(ptr %p_oob) at @main
+; CHECK-NEXT: Immediate UB detected: freeing a pointer that does not point to the start of an allocation. Pointer address: 0x18, allocation base: 0x10.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/lib_free_stack.ll b/llvm/test/tools/llubi/lib_free_stack.ll
index ca4fc72101a1c..d6f80003913e3 100644
--- a/llvm/test/tools/llubi/lib_free_stack.ll
+++ b/llvm/test/tools/llubi/lib_free_stack.ll
@@ -11,5 +11,7 @@ define i32 @main() {
 }
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %p = alloca i32, align 4 => ptr 0x8 [p]
-; CHECK-NEXT: Immediate UB detected: freeing a non-heap allocation.
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @free(ptr %p) at @main
+; CHECK-NEXT: Immediate UB detected: freeing a non-heap allocation at 0x8.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/lib_poison_argument.ll b/llvm/test/tools/llubi/lib_poison_argument.ll
index 4ebea78faf0d2..7889609e59290 100644
--- a/llvm/test/tools/llubi/lib_poison_argument.ll
+++ b/llvm/test/tools/llubi/lib_poison_argument.ll
@@ -9,5 +9,7 @@ define i32 @main() {
   ret i32 0
 }
 ; CHECK: Entering function: main
-; CHECK-NEXT: Immediate UB detected: Poison argument passed to a library call.
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %1 = call i32 @puts(ptr poison) at @main
+; CHECK-NEXT: Immediate UB detected: Poison argument passed to a library call at argument index 0.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/lib_printf_not_enough_argument.ll b/llvm/test/tools/llubi/lib_printf_not_enough_argument.ll
index 87158213b5e70..03b03cc32a512 100644
--- a/llvm/test/tools/llubi/lib_printf_not_enough_argument.ll
+++ b/llvm/test/tools/llubi/lib_printf_not_enough_argument.ll
@@ -14,5 +14,7 @@ define i32 @main() {
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %fmt = alloca [18 x i8], align 1 => ptr 0x8 [fmt]
 ; CHECK-NEXT:   store [18 x i8] c"Ints: %d, %i, %u\0A\00", ptr %fmt, align 1
-; CHECK-NEXT: Immediate UB detected: Not enough arguments provided for the format string.
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %1 = call i32 (ptr, ...) @printf(ptr %fmt, i32 42, i32 -42) at @main
+; CHECK-NEXT: Immediate UB detected: Not enough arguments provided for the format string. Required argument for 'u'.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/lib_printf_unknown_specifier.ll b/llvm/test/tools/llubi/lib_printf_unknown_specifier.ll
index d6c4cb7f90b53..29fb7c39c81e1 100644
--- a/llvm/test/tools/llubi/lib_printf_unknown_specifier.ll
+++ b/llvm/test/tools/llubi/lib_printf_unknown_specifier.ll
@@ -14,5 +14,7 @@ define i32 @main() {
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %fmt = alloca [4 x i8], align 1 => ptr 0x8 [fmt]
 ; CHECK-NEXT:   store [4 x i8] c"%m\0A\00", ptr %fmt, align 1
-; CHECK-NEXT: Immediate UB detected: Unknown or unsupported format specifier in printf.
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %1 = call i32 (ptr, ...) @printf(ptr %fmt, i32 0) at @main
+; CHECK-NEXT: Immediate UB detected: Unknown or unsupported format specifier 'm' in printf.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/lib_read_nullary_string.ll b/llvm/test/tools/llubi/lib_read_nullary_string.ll
index 2c1e354a3850e..bdf848f87da05 100644
--- a/llvm/test/tools/llubi/lib_read_nullary_string.ll
+++ b/llvm/test/tools/llubi/lib_read_nullary_string.ll
@@ -9,5 +9,7 @@ define i32 @main() {
   ret i32 0
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %1 = call i32 @puts(ptr null) at @main
 ; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/lib_uninit_string.ll b/llvm/test/tools/llubi/lib_uninit_string.ll
index 7274cfdb63363..917e2b14078cb 100644
--- a/llvm/test/tools/llubi/lib_uninit_string.ll
+++ b/llvm/test/tools/llubi/lib_uninit_string.ll
@@ -14,5 +14,7 @@ entry:
 }
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %ptr = call ptr @malloc(i64 10) => ptr 0x10 [ptr]
-; CHECK-NEXT: Immediate UB detected: Read uninitialized or poison memory while parsing C-string.
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %0 = call i32 @puts(ptr %ptr) at @main
+; CHECK-NEXT: Immediate UB detected: Read uninitialized or poison memory while parsing C-string at offset 0.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/loadstore_misaligned.ll b/llvm/test/tools/llubi/loadstore_misaligned.ll
index cc80639d773bd..bec10370e9832 100644
--- a/llvm/test/tools/llubi/loadstore_misaligned.ll
+++ b/llvm/test/tools/llubi/loadstore_misaligned.ll
@@ -10,5 +10,7 @@ define void @main() {
 ; CHECK: Entering function: main
 ; CHECK-NEXT: %alloc = alloca [2 x i32], align 8 => ptr 0x8 [alloc]
 ; CHECK-NEXT: %gep = getelementptr inbounds [2 x i32], ptr %alloc, i64 0, i64 1 => ptr 0xC [alloc + 4]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0 {{store i32 0|%res = load i32}}, ptr %gep, align 8 at @main
 ; CHECK-NEXT: Immediate UB detected: Misaligned memory access.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/loadstore_null.ll b/llvm/test/tools/llubi/loadstore_null.ll
index 7a243784e6bd6..d6ee8cc64e881 100644
--- a/llvm/test/tools/llubi/loadstore_null.ll
+++ b/llvm/test/tools/llubi/loadstore_null.ll
@@ -6,5 +6,7 @@ define void @main() {
   ret void
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0 {{store i32 0|%res = load i32}}, ptr null, align 4 at @main
 ; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/loadstore_oob1.ll b/llvm/test/tools/llubi/loadstore_oob1.ll
index 0618faa96bfde..93c2a53f17c9b 100644
--- a/llvm/test/tools/llubi/loadstore_oob1.ll
+++ b/llvm/test/tools/llubi/loadstore_oob1.ll
@@ -10,5 +10,7 @@ define void @main() {
 ; CHECK: Entering function: main
 ; CHECK-NEXT: %alloc = alloca [2 x i32], align 4 => ptr 0x8 [alloc]
 ; CHECK-NEXT: %gep = getelementptr inbounds [2 x i32], ptr %alloc, i64 0, i64 2 => ptr 0x10 [alloc + 8]
-; CHECK-NEXT: Immediate UB detected: Memory access is out of bounds.
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   {{store i32 0|%res = load i32}}, ptr %gep, align 4 at @main
+; CHECK-NEXT: Immediate UB detected: Memory access is out of bounds. Accessed size: 4, Address: 0x10, Object base: 0x8, Object size: 8.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/loadstore_poison.ll b/llvm/test/tools/llubi/loadstore_poison.ll
index 44c72aa803709..694c52da99d3c 100644
--- a/llvm/test/tools/llubi/loadstore_poison.ll
+++ b/llvm/test/tools/llubi/loadstore_poison.ll
@@ -6,5 +6,7 @@ define void @main() {
   ret void
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0 {{store i32 0|%res = load i32}}, ptr poison, align 4 at @main
 ; CHECK-NEXT: Immediate UB detected: Invalid memory access with a poison pointer.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/loadstore_uaf.ll b/llvm/test/tools/llubi/loadstore_uaf.ll
index 21b86552275b6..ab81a75fd2cb7 100644
--- a/llvm/test/tools/llubi/loadstore_uaf.ll
+++ b/llvm/test/tools/llubi/loadstore_uaf.ll
@@ -17,5 +17,7 @@ define void @main() {
 ; CHECK-NEXT:   ret ptr %alloc
 ; CHECK-NEXT: Exiting function: stack_object
 ; CHECK-NEXT:   %alloc = call ptr @stack_object() => ptr 0x8 [dangling]
-; CHECK-NEXT: Immediate UB detected: Try to access a dead memory object.
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   {{store i32 0|%res = load i32}}, ptr %alloc, align 4 at @main
+; CHECK-NEXT: Immediate UB detected: Try to access a dead memory object at address 0x8.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/stack_overflow.ll b/llvm/test/tools/llubi/stack_overflow.ll
index a241d176ad696..f38350dd4e7d8 100644
--- a/llvm/test/tools/llubi/stack_overflow.ll
+++ b/llvm/test/tools/llubi/stack_overflow.ll
@@ -103,5 +103,16 @@ entry:
 ; CHECK-NEXT:   br i1 %cmp, label %if.then, label %if.else jump to %if.then
 ; CHECK-NEXT:   %sub1 = sub i32 %n, 1 => i32 41
 ; CHECK-NEXT:   %sub2 = sub i32 %n, 2 => i32 40
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %call1 = call i32 @fib(i32 %sub1) at @fib
+; CHECK-NEXT: #1   %call1 = call i32 @fib(i32 %sub1) at @fib
+; CHECK-NEXT: #2   %call1 = call i32 @fib(i32 %sub1) at @fib
+; CHECK-NEXT: #3   %call1 = call i32 @fib(i32 %sub1) at @fib
+; CHECK-NEXT: #4   %call1 = call i32 @fib(i32 %sub1) at @fib
+; CHECK-NEXT: #5   %call1 = call i32 @fib(i32 %sub1) at @fib
+; CHECK-NEXT: #6   %call1 = call i32 @fib(i32 %sub1) at @fib
+; CHECK-NEXT: #7   %call1 = call i32 @fib(i32 %sub1) at @fib
+; CHECK-NEXT: #8   %call1 = call i32 @fib(i32 %sub1) at @fib
+; CHECK-NEXT: #9   %res2 = call i32 @fib(i32 50) at @main
 ; CHECK-NEXT: Error: Maximum stack depth exceeded.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/store_dead.ll b/llvm/test/tools/llubi/store_dead.ll
index 8bd15beefb8d3..53ad2525369cb 100644
--- a/llvm/test/tools/llubi/store_dead.ll
+++ b/llvm/test/tools/llubi/store_dead.ll
@@ -14,5 +14,7 @@ define void @main() {
 ; CHECK-NEXT:   call void @llvm.lifetime.start.p0(ptr %alloc)
 ; CHECK-NEXT:   store i32 0, ptr %alloc, align 4
 ; CHECK-NEXT:   call void @llvm.lifetime.end.p0(ptr %alloc)
-; CHECK-NEXT: Immediate UB detected: Try to access a dead memory object.
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i32 0, ptr %alloc, align 4 at @main
+; CHECK-NEXT: Immediate UB detected: Try to access a dead memory object at address 0x8.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/switch_poison.ll b/llvm/test/tools/llubi/switch_poison.ll
index 32316a467063a..946643ec02567 100644
--- a/llvm/test/tools/llubi/switch_poison.ll
+++ b/llvm/test/tools/llubi/switch_poison.ll
@@ -9,5 +9,8 @@ exit:
   ret void
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   switch i32 poison, label %exit [
+; CHECK-NEXT:   ] at @main
 ; CHECK-NEXT: Immediate UB detected: Switch on poison condition.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/unreachable.ll b/llvm/test/tools/llubi/unreachable.ll
index c24a02eb5eb86..bf98ec7361c4f 100644
--- a/llvm/test/tools/llubi/unreachable.ll
+++ b/llvm/test/tools/llubi/unreachable.ll
@@ -5,5 +5,7 @@ define void @main() {
   unreachable
 }
 ; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   unreachable at @main
 ; CHECK-NEXT: Immediate UB detected: Unreachable code.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/tools/llubi/lib/ExecutorBase.cpp b/llvm/tools/llubi/lib/ExecutorBase.cpp
index f1247cd6bf69e..2146bf19d208f 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.cpp
+++ b/llvm/tools/llubi/lib/ExecutorBase.cpp
@@ -27,19 +27,29 @@ Frame::Frame(Function &F, CallBase *CallSite, Frame *LastFrame,
     ValueMap[&Arg] = Args[Arg.getArgNo()];
 }
 
-void ExecutorBase::reportImmediateUB(StringRef Msg) {
+DiagnosticReporter ExecutorBase::reportImmediateUB() {
+  return DiagnosticReporter(*this, DiagnosticKind::ImmediateUB);
+}
+
+DiagnosticReporter ExecutorBase::reportError() {
+  return DiagnosticReporter(*this, DiagnosticKind::Error);
+}
+
+void ExecutorBase::reportImmediateUBString(StringRef Msg) {
   // Check if we have already reported an immediate UB.
   if (hasProgramExited())
     return;
+  dumpStackTrace();
   requestProgramExit(ProgramExitInfo::ProgramExitKind::Failed);
   // TODO: Provide stack trace information.
   Handler.onImmediateUB(Msg);
 }
 
-void ExecutorBase::reportError(StringRef Msg) {
+void ExecutorBase::reportErrorString(StringRef Msg) {
   // Check if we have already reported an error message.
   if (hasProgramExited())
     return;
+  dumpStackTrace();
   requestProgramExit(ProgramExitInfo::ProgramExitKind::Failed);
   Handler.onError(Msg);
 }
@@ -54,24 +64,37 @@ std::optional<uint64_t> ExecutorBase::verifyMemAccess(const MemoryObject &MO,
   // undefined behavior.
   if (IsStore ? MO.getState() != MemoryObjectState::Alive
               : MO.getState() == MemoryObjectState::Freed) {
-    reportImmediateUB("Try to access a dead memory object.");
+    reportImmediateUB() << "Try to access a dead memory object at address 0x"
+                        << Twine::utohexstr(Address.getZExtValue()) << ".";
     return std::nullopt;
   }
 
   if (Address.countr_zero() < Log2(Alignment)) {
-    reportImmediateUB("Misaligned memory access.");
+    reportImmediateUB() << "Misaligned memory access. Address: 0x"
+                        << Twine::utohexstr(Address.getZExtValue())
+                        << ", Required alignment: " << Alignment.value() << ".";
     return std::nullopt;
   }
 
   if (AccessSize > MO.getSize() || Address.ult(MO.getAddress())) {
-    reportImmediateUB("Memory access is out of bounds.");
+    reportImmediateUB() << "Memory access is out of bounds. Accessed size: "
+                        << AccessSize << ", Address: 0x"
+                        << Twine::utohexstr(Address.getZExtValue())
+                        << ", Object base: 0x"
+                        << Twine::utohexstr(MO.getAddress())
+                        << ", Object size: " << MO.getSize() << ".";
     return std::nullopt;
   }
 
   APInt Offset = Address - MO.getAddress();
 
   if (Offset.ugt(MO.getSize() - AccessSize)) {
-    reportImmediateUB("Memory access is out of bounds.");
+    reportImmediateUB() << "Memory access is out of bounds. Accessed size: "
+                        << AccessSize << ", Address: 0x"
+                        << Twine::utohexstr(Address.getZExtValue())
+                        << ", Object base: 0x"
+                        << Twine::utohexstr(MO.getAddress())
+                        << ", Object size: " << MO.getSize() << ".";
     return std::nullopt;
   }
 
@@ -80,14 +103,14 @@ std::optional<uint64_t> ExecutorBase::verifyMemAccess(const MemoryObject &MO,
 
 AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy) {
   if (Ptr.isPoison()) {
-    reportImmediateUB("Invalid memory access with a poison pointer.");
+    reportImmediateUB() << "Invalid memory access with a poison pointer.";
     return AnyValue::getPoisonValue(Ctx, ValTy);
   }
   auto &PtrVal = Ptr.asPointer();
   auto *MO = PtrVal.getMemoryObject();
   if (!MO) {
-    reportImmediateUB(
-        "Invalid memory access via a pointer with nullary provenance.");
+    reportImmediateUB()
+        << "Invalid memory access via a pointer with nullary provenance.";
     return AnyValue::getPoisonValue(Ctx, ValTy);
   }
   // TODO: pointer capability check
@@ -107,14 +130,14 @@ AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy) {
 void ExecutorBase::store(const AnyValue &Ptr, Align Alignment,
                          const AnyValue &Val, Type *ValTy) {
   if (Ptr.isPoison()) {
-    reportImmediateUB("Invalid memory access with a poison pointer.");
+    reportImmediateUB() << "Invalid memory access with a poison pointer.";
     return;
   }
   auto &PtrVal = Ptr.asPointer();
   auto *MO = PtrVal.getMemoryObject();
   if (!MO) {
-    reportImmediateUB(
-        "Invalid memory access via a pointer with nullary provenance.");
+    reportImmediateUB()
+        << "Invalid memory access via a pointer with nullary provenance.";
     return;
   }
   // TODO: pointer capability check
@@ -144,4 +167,19 @@ std::optional<ProgramExitInfo> ExecutorBase::getExitInfo() const {
 unsigned ExecutorBase::getIntSize() const {
   return CurrentFrame->TLI.getIntSize();
 }
+
+void ExecutorBase::dumpStackTrace() const {
+  errs() << "Stacktrace:\n";
+  const Frame *Frm = CurrentFrame;
+  unsigned Index = 0;
+  while (Frm != nullptr) {
+    if (Frm->BB) {
+      Instruction &Inst = *Frm->PC;
+      errs() << "#" << Index++ << " " << Inst << " at ";
+      Inst.getFunction()->printAsOperand(errs(), /*PrintType=*/false);
+      errs() << "\n";
+    }
+    Frm = Frm->LastFrame;
+  }
+}
 } // namespace llvm::ubi
diff --git a/llvm/tools/llubi/lib/ExecutorBase.h b/llvm/tools/llubi/lib/ExecutorBase.h
index 0fa73d9294e07..00e399bd30f9d 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.h
+++ b/llvm/tools/llubi/lib/ExecutorBase.h
@@ -15,7 +15,10 @@
 
 #include "Context.h"
 #include "Value.h"
+#include "llvm/Support/raw_ostream.h"
 #include <optional>
+#include <string>
+#include <utility>
 
 namespace llvm::ubi {
 
@@ -68,7 +71,16 @@ struct Frame {
         const TargetLibraryInfoImpl &TLIImpl);
 };
 
+enum class DiagnosticKind {
+  ImmediateUB,
+  Error,
+};
+
+class DiagnosticReporter;
+
 class ExecutorBase {
+  friend class DiagnosticReporter;
+
 protected:
   Context &Ctx;
   EventHandler &Handler;
@@ -79,9 +91,13 @@ class ExecutorBase {
       : Ctx(C), Handler(H), ExitInfo(std::nullopt) {}
   ~ExecutorBase() = default;
 
+private:
+  void reportImmediateUBString(StringRef Msg);
+  void reportErrorString(StringRef Msg);
+
 public:
-  void reportImmediateUB(StringRef Msg);
-  void reportError(StringRef Msg);
+  DiagnosticReporter reportImmediateUB();
+  DiagnosticReporter reportError();
 
   /// Check if the upcoming memory access is valid. Returns the offset relative
   /// to the underlying object if it is valid.
@@ -102,6 +118,35 @@ class ExecutorBase {
   std::optional<ProgramExitInfo> getExitInfo() const;
 
   unsigned getIntSize() const;
+
+  void dumpStackTrace() const;
+};
+
+class DiagnosticReporter {
+  ExecutorBase &Executor;
+  std::string Buf;
+  raw_string_ostream OS;
+  DiagnosticKind Kind;
+
+public:
+  DiagnosticReporter(ExecutorBase &E, DiagnosticKind K)
+      : Executor(E), OS(Buf), Kind(K) {}
+  ~DiagnosticReporter() {
+    OS.flush();
+    switch (Kind) {
+    case DiagnosticKind::ImmediateUB:
+      Executor.reportImmediateUBString(Buf);
+      break;
+    case DiagnosticKind::Error:
+      Executor.reportErrorString(Buf);
+      break;
+    }
+  }
+
+  template <typename T> DiagnosticReporter &operator<<(const T &Val) {
+    OS << Val;
+    return *this;
+  }
 };
 
 } // namespace llvm::ubi
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index cc8b70b9875f6..a77a8488c5f33 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -378,7 +378,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   // a poison is found.
   bool getBooleanNonPoison(BooleanKind Boolean) {
     if (Boolean == BooleanKind::Poison)
-      reportImmediateUB("Unexpected poison boolean value");
+      reportImmediateUB() << "Unexpected poison boolean value";
     return Boolean == BooleanKind::True;
   }
 
@@ -410,7 +410,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
       jumpTo(BI, BI.getSuccessor(1));
       return;
     case BooleanKind::Poison:
-      reportImmediateUB("Branch on poison condition.");
+      reportImmediateUB() << "Branch on poison condition.";
       return;
     }
   }
@@ -418,7 +418,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   void visitSwitchInst(SwitchInst &SI) {
     auto &Cond = getValue(SI.getCondition());
     if (Cond.isPoison()) {
-      reportImmediateUB("Switch on poison condition.");
+      reportImmediateUB() << "Switch on poison condition.";
       return;
     }
     for (auto &Case : SI.cases()) {
@@ -431,7 +431,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitUnreachableInst(UnreachableInst &) {
-    reportImmediateUB("Unreachable code.");
+    reportImmediateUB() << "Unreachable code.";
   }
 
   void visitCallBrInst(CallBrInst &CI) {
@@ -447,7 +447,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   void visitIndirectBrInst(IndirectBrInst &IBI) {
     auto &Target = getValue(IBI.getAddress());
     if (Target.isPoison()) {
-      reportImmediateUB("Indirect branch on poison.");
+      reportImmediateUB() << "Indirect branch on poison.";
       return;
     }
     if (BasicBlock *DestBB = Ctx.getTargetBlock(Target.asPointer())) {
@@ -455,11 +455,11 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
                  [DestBB](BasicBlock *Succ) { return Succ == DestBB; }))
         jumpTo(IBI, DestBB);
       else
-        reportImmediateUB("Indirect branch on unlisted target BB.");
+        reportImmediateUB() << "Indirect branch on unlisted target BB.";
 
       return;
     }
-    reportImmediateUB("Indirect branch on invalid target BB.");
+    reportImmediateUB() << "Indirect branch on invalid target BB.";
   }
 
   void returnFromCallee() {
@@ -488,7 +488,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
         break;
       case BooleanKind::False:
       case BooleanKind::Poison:
-        reportImmediateUB("Assume on false or poison condition.");
+        reportImmediateUB() << "Assume on false or poison condition.";
         break;
       }
       // TODO: handle llvm.assume with operand bundles
@@ -941,17 +941,20 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
 
       auto &CalleeVal = getValue(CalledOperand);
       if (CalleeVal.isPoison()) {
-        reportImmediateUB("Indirect call through poison function pointer.");
+        reportImmediateUB() << "Indirect call through poison function pointer.";
         return;
       }
       Callee = Ctx.getTargetFunction(CalleeVal.asPointer());
       if (!Callee) {
-        reportImmediateUB("Indirect call through invalid function pointer.");
+        reportImmediateUB()
+            << "Indirect call through invalid function pointer.";
         return;
       }
       if (Callee->getFunctionType() != CB.getFunctionType()) {
-        reportImmediateUB("Indirect call through a function pointer with "
-                          "mismatched signature.");
+        reportImmediateUB() << "Indirect call through a function pointer with "
+                               "mismatched signature. Expected: "
+                            << *CB.getFunctionType()
+                            << ", Actual: " << *Callee->getFunctionType();
         return;
       }
     }
@@ -972,7 +975,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     } else {
       uint32_t MaxStackDepth = Ctx.getMaxStackDepth();
       if (MaxStackDepth && CallStack.size() >= MaxStackDepth) {
-        reportError("Maximum stack depth exceeded.");
+        reportError() << "Maximum stack depth exceeded.";
         return;
       }
       assert(!Callee->empty() && "Expected a defined function.");
@@ -1014,23 +1017,23 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {
       // Priority: Immediate UB > poison > normal value
       if (RHS.isPoison()) {
-        reportImmediateUB("Division by zero (refine RHS to 0).");
+        reportImmediateUB() << "Division by zero (refine RHS to 0).";
         return AnyValue::poison();
       }
       const APInt &RHSVal = RHS.asInteger();
       if (RHSVal.isZero()) {
-        reportImmediateUB("Division by zero.");
+        reportImmediateUB() << "Division by zero.";
         return AnyValue::poison();
       }
       if (LHS.isPoison()) {
         if (RHSVal.isAllOnes())
-          reportImmediateUB(
-              "Signed division overflow (refine LHS to INT_MIN).");
+          reportImmediateUB()
+              << "Signed division overflow (refine LHS to INT_MIN).";
         return AnyValue::poison();
       }
       const APInt &LHSVal = LHS.asInteger();
       if (LHSVal.isMinSignedValue() && RHSVal.isAllOnes()) {
-        reportImmediateUB("Signed division overflow.");
+        reportImmediateUB() << "Signed division overflow.";
         return AnyValue::poison();
       }
 
@@ -1050,23 +1053,24 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {
       // Priority: Immediate UB > poison > normal value
       if (RHS.isPoison()) {
-        reportImmediateUB("Division by zero (refine RHS to 0).");
+        reportImmediateUB() << "Division by zero (refine RHS to 0).";
         return AnyValue::poison();
       }
       const APInt &RHSVal = RHS.asInteger();
       if (RHSVal.isZero()) {
-        reportImmediateUB("Division by zero.");
+        reportImmediateUB() << "Division by zero.";
         return AnyValue::poison();
       }
       if (LHS.isPoison()) {
         if (RHSVal.isAllOnes())
-          reportImmediateUB(
-              "Signed division overflow (refine LHS to INT_MIN).");
+          reportImmediateUB()
+              << "Signed division overflow (refine LHS to INT_MIN).";
         return AnyValue::poison();
       }
       const APInt &LHSVal = LHS.asInteger();
       if (LHSVal.isMinSignedValue() && RHSVal.isAllOnes()) {
-        reportImmediateUB("Signed division overflow.");
+        reportImmediateUB() << "Signed division overflow. LHS: " << LHSVal
+                            << ", RHS: " << RHSVal;
         return AnyValue::poison();
       }
 
@@ -1078,12 +1082,12 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {
       // Priority: Immediate UB > poison > normal value
       if (RHS.isPoison()) {
-        reportImmediateUB("Division by zero (refine RHS to 0).");
+        reportImmediateUB() << "Division by zero (refine RHS to 0).";
         return AnyValue::poison();
       }
       const APInt &RHSVal = RHS.asInteger();
       if (RHSVal.isZero()) {
-        reportImmediateUB("Division by zero.");
+        reportImmediateUB() << "Division by zero.";
         return AnyValue::poison();
       }
       if (LHS.isPoison())
@@ -1106,12 +1110,12 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {
       // Priority: Immediate UB > poison > normal value
       if (RHS.isPoison()) {
-        reportImmediateUB("Division by zero (refine RHS to 0).");
+        reportImmediateUB() << "Division by zero (refine RHS to 0).";
         return AnyValue::poison();
       }
       const APInt &RHSVal = RHS.asInteger();
       if (RHSVal.isZero()) {
-        reportImmediateUB("Division by zero.");
+        reportImmediateUB() << "Division by zero.";
         return AnyValue::poison();
       }
       if (LHS.isPoison())
@@ -1257,20 +1261,22 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     if (AI.isArrayAllocation()) {
       auto &Size = getValue(AI.getArraySize());
       if (Size.isPoison()) {
-        reportImmediateUB("Alloca with poison array size.");
+        reportImmediateUB() << "Alloca with poison array size.";
         return;
       }
       if (Size.asInteger().getActiveBits() > 64) {
-        reportImmediateUB(
-            "Alloca with large array size that overflows uint64_t.");
+        reportImmediateUB()
+            << "Alloca with large array size that overflows uint64_t. Size: "
+            << Size.asInteger();
         return;
       }
       bool Overflowed = false;
       AllocSize = SaturatingMultiply(AllocSize, Size.asInteger().getZExtValue(),
                                      &Overflowed);
       if (Overflowed) {
-        reportImmediateUB(
-            "Alloca with allocation size that overflows uint64_t.");
+        reportImmediateUB()
+            << "Alloca with allocation size that overflows uint64_t. Size: "
+            << Size.asInteger();
         return;
       }
     }
@@ -1284,7 +1290,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
                                             : MemInitKind::Uninitialized,
                             MemAllocKind::Stack);
     if (!Obj) {
-      reportError("Insufficient stack space.");
+      reportError() << "Insufficient stack space.";
       return;
     }
     CurrentFrame->Allocas.push_back(Obj);
@@ -1502,7 +1508,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
         assert(Top.State == FrameState::Running &&
                "Expected to be in running state.");
         if (MaxSteps != 0 && Steps >= MaxSteps) {
-          reportError("Exceeded maximum number of execution steps.");
+          reportError() << "Exceeded maximum number of execution steps.";
           break;
         }
         ++Steps;
diff --git a/llvm/tools/llubi/lib/Library.cpp b/llvm/tools/llubi/lib/Library.cpp
index fb626e2ad7dd7..a1e58e4e57f47 100644
--- a/llvm/tools/llubi/lib/Library.cpp
+++ b/llvm/tools/llubi/lib/Library.cpp
@@ -31,9 +31,8 @@ Library::Library(Context &Ctx, EventHandler &Handler, const DataLayout &DL,
 std::optional<std::string> Library::readStringFromMemory(const Pointer &Ptr) {
   auto *MO = Ptr.getMemoryObject();
   if (!MO) {
-    Executor.reportImmediateUB(
-        "Invalid memory access via a pointer with nullary "
-        "provenance.");
+    Executor.reportImmediateUB()
+        << "Invalid memory access via a pointer with nullary provenance.";
     return std::nullopt;
   }
 
@@ -49,8 +48,10 @@ std::optional<std::string> Library::readStringFromMemory(const Pointer &Ptr) {
 
     Byte B = (*MO)[*ValidOffset];
     if (B.ConcreteMask != 0xFF) {
-      Executor.reportImmediateUB("Read uninitialized or poison memory while "
-                                 "parsing C-string.");
+      Executor.reportImmediateUB()
+          << "Read uninitialized or poison memory while "
+             "parsing C-string at offset "
+          << Offset << ".";
       return std::nullopt;
     }
 
@@ -83,7 +84,7 @@ AnyValue Library::executeMalloc(StringRef Name, Type *Type,
     if (AllocKind == MemAllocKind::New || AllocKind == MemAllocKind::NewArray) {
       // FIXME: As llubi doesn't support stack unwinding yet, we report an error
       // when new/new[] fails.
-      Executor.reportError("Insufficient heap space.");
+      Executor.reportError() << "Insufficient heap space.";
       return AnyValue::poison();
     }
     return AnyValue::getNullValue(Ctx, Type);
@@ -129,24 +130,31 @@ AnyValue Library::executeFree(ArrayRef<AnyValue> Args) {
 
   MemoryObject *Obj = Ptr.getMemoryObject();
   if (!Obj) {
-    Executor.reportImmediateUB("freeing a pointer with nullary provenance.");
+    Executor.reportImmediateUB()
+        << "freeing a pointer with nullary provenance.";
     return AnyValue::poison();
   }
 
   if (const uint64_t Address = Ptr.address().getZExtValue();
       Address != Obj->getAddress()) {
-    Executor.reportImmediateUB(
-        "freeing a pointer that does not point to the start of an allocation.");
+    Executor.reportImmediateUB()
+        << "freeing a pointer that does not point to "
+           "the start of an allocation. Pointer address: 0x"
+        << Twine::utohexstr(Address) << ", allocation base: 0x"
+        << Twine::utohexstr(Obj->getAddress()) << ".";
     return AnyValue::poison();
   }
 
   if (Obj->getState() == MemoryObjectState::Freed) {
-    Executor.reportImmediateUB("double-freeing a memory object.");
+    Executor.reportImmediateUB()
+        << "double-freeing a memory object allocated at 0x"
+        << Twine::utohexstr(Obj->getAddress()) << ".";
     return AnyValue::poison();
   }
 
   if (!Obj->isHeapAllocated()) {
-    Executor.reportImmediateUB("freeing a non-heap allocation.");
+    Executor.reportImmediateUB() << "freeing a non-heap allocation at 0x"
+                                 << Twine::utohexstr(Obj->getAddress()) << ".";
     return AnyValue::poison();
   }
 
@@ -155,7 +163,9 @@ AnyValue Library::executeFree(ArrayRef<AnyValue> Args) {
   // function comes from a different family (C++ delete, etc.)
 
   if (!Ctx.free(*Obj)) {
-    Executor.reportImmediateUB("freeing an invalid pointer.");
+    Executor.reportImmediateUB()
+        << "freeing an invalid pointer at 0x"
+        << Twine::utohexstr(Ptr.address().getZExtValue()) << ".";
     return AnyValue::poison();
   }
 
@@ -206,9 +216,8 @@ AnyValue Library::executePrintf(ArrayRef<AnyValue> Args) {
       ++I;
 
     if (I >= FormatStr.size()) {
-      Executor.reportImmediateUB(
-          "Invalid format string in printf: missing conversion "
-          "specifier.");
+      Executor.reportImmediateUB()
+          << "Invalid format string in printf: missing conversion specifier.";
       return AnyValue::poison();
     }
 
@@ -220,14 +229,17 @@ AnyValue Library::executePrintf(ArrayRef<AnyValue> Args) {
         CleanChunk.end());
 
     if (ArgIndex >= Args.size()) {
-      Executor.reportImmediateUB(
-          "Not enough arguments provided for the format string.");
+      Executor.reportImmediateUB() << "Not enough arguments provided for the "
+                                      "format string. Required argument for '"
+                                   << Specifier << "'.";
       return AnyValue::poison();
     }
 
     const auto &Arg = Args[ArgIndex++];
     if (Arg.isPoison()) {
-      Executor.reportImmediateUB("Poison argument passed to printf.");
+      Executor.reportImmediateUB()
+          << "Poison argument passed to printf for format specifier '"
+          << Specifier << "' at argument index " << ArgIndex << ".";
       return AnyValue::poison();
     }
 
@@ -290,8 +302,9 @@ AnyValue Library::executePrintf(ArrayRef<AnyValue> Args) {
       break;
     }
     default:
-      Executor.reportImmediateUB(
-          "Unknown or unsupported format specifier in printf.");
+      Executor.reportImmediateUB()
+          << "Unknown or unsupported format specifier '" << Specifier
+          << "' in printf.";
       return AnyValue::poison();
     }
   }
@@ -322,11 +335,15 @@ AnyValue Library::executeTerminate() {
 std::optional<AnyValue> Library::executeLibcall(LibFunc LF, StringRef Name,
                                                 Type *Type,
                                                 ArrayRef<AnyValue> Args) {
+  unsigned Index = 0;
   for (const AnyValue &Arg : Args) {
     if (Arg.isPoison()) {
-      Executor.reportImmediateUB("Poison argument passed to a library call.");
+      Executor.reportImmediateUB()
+          << "Poison argument passed to a library call at argument index "
+          << Index << ".";
       return AnyValue::poison();
     }
+    ++Index;
   }
 
   switch (LF) {

>From d9a9f5e89a1473f5907a780aa018549e4662958e Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhigec_cpp at outlook.com>
Date: Sat, 2 May 2026 21:19:34 +0800
Subject: [PATCH 2/3] [llubi] Remove finished TODO

---
 llvm/tools/llubi/lib/ExecutorBase.cpp | 1 -
 1 file changed, 1 deletion(-)

diff --git a/llvm/tools/llubi/lib/ExecutorBase.cpp b/llvm/tools/llubi/lib/ExecutorBase.cpp
index 2146bf19d208f..9cf1297aa518f 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.cpp
+++ b/llvm/tools/llubi/lib/ExecutorBase.cpp
@@ -41,7 +41,6 @@ void ExecutorBase::reportImmediateUBString(StringRef Msg) {
     return;
   dumpStackTrace();
   requestProgramExit(ProgramExitInfo::ProgramExitKind::Failed);
-  // TODO: Provide stack trace information.
   Handler.onImmediateUB(Msg);
 }
 

>From 823eb23f7f3043aecaccd375a41c259ce260e06f Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhigec_cpp at outlook.com>
Date: Sat, 2 May 2026 21:48:06 +0800
Subject: [PATCH 3/3] [llubi] Minor fixes

---
 llvm/tools/llubi/lib/ExecutorBase.cpp | 10 +++++-----
 llvm/tools/llubi/lib/ExecutorBase.h   |  8 +++++++-
 2 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/llvm/tools/llubi/lib/ExecutorBase.cpp b/llvm/tools/llubi/lib/ExecutorBase.cpp
index 9cf1297aa518f..233497e041b00 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.cpp
+++ b/llvm/tools/llubi/lib/ExecutorBase.cpp
@@ -169,16 +169,16 @@ unsigned ExecutorBase::getIntSize() const {
 
 void ExecutorBase::dumpStackTrace() const {
   errs() << "Stacktrace:\n";
-  const Frame *Frm = CurrentFrame;
+  const Frame *TheFrame = CurrentFrame;
   unsigned Index = 0;
-  while (Frm != nullptr) {
-    if (Frm->BB) {
-      Instruction &Inst = *Frm->PC;
+  while (TheFrame != nullptr) {
+    if (TheFrame->BB) {
+      Instruction &Inst = *TheFrame->PC;
       errs() << "#" << Index++ << " " << Inst << " at ";
       Inst.getFunction()->printAsOperand(errs(), /*PrintType=*/false);
       errs() << "\n";
     }
-    Frm = Frm->LastFrame;
+    TheFrame = TheFrame->LastFrame;
   }
 }
 } // namespace llvm::ubi
diff --git a/llvm/tools/llubi/lib/ExecutorBase.h b/llvm/tools/llubi/lib/ExecutorBase.h
index 00e399bd30f9d..64933929fdc35 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.h
+++ b/llvm/tools/llubi/lib/ExecutorBase.h
@@ -131,8 +131,14 @@ class DiagnosticReporter {
 public:
   DiagnosticReporter(ExecutorBase &E, DiagnosticKind K)
       : Executor(E), OS(Buf), Kind(K) {}
+
+  DiagnosticReporter(const DiagnosticReporter &) = delete;
+  DiagnosticReporter(DiagnosticReporter &&) noexcept = delete;
+
+  DiagnosticReporter &operator=(const DiagnosticReporter &) = delete;
+  DiagnosticReporter &operator=(DiagnosticReporter &&) noexcept = delete;
+
   ~DiagnosticReporter() {
-    OS.flush();
     switch (Kind) {
     case DiagnosticKind::ImmediateUB:
       Executor.reportImmediateUBString(Buf);



More information about the llvm-commits mailing list