[llvm] [llubi] Add support for exposed provenance (PR #200596)

Yingwei Zheng via llvm-commits llvm-commits at lists.llvm.org
Sat Jun 13 08:17:17 PDT 2026


https://github.com/dtcxzyw updated https://github.com/llvm/llvm-project/pull/200596

>From 6cd61b1ad5164e58a1f1984c1bb59e2d3c10384f Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Sat, 30 May 2026 10:36:47 +0800
Subject: [PATCH 1/7] [llubi] Add extra padding to memory objects

---
 llvm/test/tools/llubi/alloca.ll            |  8 +++---
 llvm/test/tools/llubi/attributes.ll        |  2 +-
 llvm/test/tools/llubi/gep.ll               |  6 ++--
 llvm/test/tools/llubi/lib_abort.ll         |  2 +-
 llvm/test/tools/llubi/lib_exit.ll          |  2 +-
 llvm/test/tools/llubi/lib_io.ll            |  4 +--
 llvm/test/tools/llubi/lib_printf_format.ll | 18 ++++++------
 llvm/test/tools/llubi/lib_terminate.ll     |  2 +-
 llvm/test/tools/llubi/loadstore_be.ll      | 30 ++++++++++----------
 llvm/test/tools/llubi/loadstore_le.ll      | 32 +++++++++++-----------
 llvm/test/tools/llubi/metadata.ll          | 16 +++++------
 llvm/tools/llubi/lib/Context.cpp           |  4 ++-
 12 files changed, 64 insertions(+), 62 deletions(-)

diff --git a/llvm/test/tools/llubi/alloca.ll b/llvm/test/tools/llubi/alloca.ll
index e45d499d3205b..3445ced01331b 100644
--- a/llvm/test/tools/llubi/alloca.ll
+++ b/llvm/test/tools/llubi/alloca.ll
@@ -30,12 +30,12 @@ define void @main() {
 ; CHECK-NEXT:   ret i32 4
 ; CHECK-NEXT: Exiting function: count
 ; CHECK-NEXT:   %count = call i32 @count() => i32 4
-; CHECK-NEXT:   %alloc_dyn = alloca i32, i32 %count, align 4 => ptr 0x44 [alloc_dyn]
-; CHECK-NEXT:   %alloc_struct = alloca %struct, align 8 => ptr 0x58 [alloc_struct]
+; CHECK-NEXT:   %alloc_dyn = alloca i32, i32 %count, align 4 => ptr 0x48 [alloc_dyn]
+; CHECK-NEXT:   %alloc_struct = alloca %struct, align 8 => ptr 0x60 [alloc_struct]
 ; CHECK-NEXT: Entering function: stack_address
-; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x70 [alloc]
+; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x7C [alloc]
 ; CHECK-NEXT:   ret ptr %alloc
 ; CHECK-NEXT: Exiting function: stack_address
-; CHECK-NEXT:   %ptr = call ptr @stack_address() => ptr 0x70 [alloc (dangling)]
+; CHECK-NEXT:   %ptr = call ptr @stack_address() => ptr 0x7C [alloc (dangling)]
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/attributes.ll b/llvm/test/tools/llubi/attributes.ll
index cc8fcf62a46f9..87057ab816ba7 100644
--- a/llvm/test/tools/llubi/attributes.ll
+++ b/llvm/test/tools/llubi/attributes.ll
@@ -320,7 +320,7 @@ define void @main() {
 ; CHECK-NEXT:   ret ptr %p
 ; CHECK-NEXT: Exiting function: identity_dereferenceable_or_null
 ; CHECK-NEXT:   %deref_or_null_mixed = call dereferenceable(1) dereferenceable_or_null(1) ptr @identity_dereferenceable_or_null(ptr dereferenceable(1) dereferenceable_or_null(1) %alloc) => ptr 0x8 [alloc]
-; CHECK-NEXT:   %fmt_n_out = alloca [6 x i8], align 1 => ptr 0xC [fmt_n_out]
+; CHECK-NEXT:   %fmt_n_out = alloca [6 x i8], align 1 => ptr 0xD [fmt_n_out]
 ; CHECK-NEXT:   store [6 x i8] c"N=%d\0A\00", ptr %fmt_n_out, align 1
 ; CHECK-NEXT: N=6
 ; CHECK-NEXT:   %res = call noundef range(i32 0, 15) i32 (ptr, ...) @printf(ptr noundef nonnull %fmt_n_out, i32 noundef range(i32 0, 15) 6) => i32 4
diff --git a/llvm/test/tools/llubi/gep.ll b/llvm/test/tools/llubi/gep.ll
index 3426249fa138c..8cfa63b105b4b 100644
--- a/llvm/test/tools/llubi/gep.ll
+++ b/llvm/test/tools/llubi/gep.ll
@@ -117,11 +117,11 @@ define void @main() {
 ; CHECK-NEXT:   %gep_inbounds_invalid2 = getelementptr inbounds i8, ptr %alloc, i64 5 => poison
 ; CHECK-NEXT:   %gep_inbounds_invalid3 = getelementptr inbounds %struct, ptr %alloc_struct, i64 1, i32 1, i32 -2 => poison
 ; CHECK-NEXT: Entering function: dead_stack_object
-; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x28 [alloc]
+; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x2C [alloc]
 ; CHECK-NEXT:   ret ptr %alloc
 ; CHECK-NEXT: Exiting function: dead_stack_object
-; CHECK-NEXT:   %dead_stack_ptr = call ptr @dead_stack_object() => ptr 0x28 [alloc (dangling)]
-; CHECK-NEXT:   %gep_inbounds_valid4 = getelementptr inbounds i8, ptr %dead_stack_ptr, i64 4 => ptr 0x2C [alloc + 4 (dangling)]
+; CHECK-NEXT:   %dead_stack_ptr = call ptr @dead_stack_object() => ptr 0x2C [alloc (dangling)]
+; CHECK-NEXT:   %gep_inbounds_valid4 = getelementptr inbounds i8, ptr %dead_stack_ptr, i64 4 => ptr 0x30 [alloc + 4 (dangling)]
 ; CHECK-NEXT:   %gep_inbounds_invalid4 = getelementptr inbounds i8, ptr %dead_stack_ptr, i64 5 => poison
 ; CHECK-NEXT:   %gep_nusw_valid1 = getelementptr nusw i8, ptr %alloc, i64 -1 => ptr 0x7 [alloc + -1]
 ; CHECK-NEXT:   %gep_nusw_invalid1 = getelementptr nusw i8, ptr %large_address, i64 -2147483649 => poison
diff --git a/llvm/test/tools/llubi/lib_abort.ll b/llvm/test/tools/llubi/lib_abort.ll
index 78ed03c6c70ca..f1d3b829d8076 100644
--- a/llvm/test/tools/llubi/lib_abort.ll
+++ b/llvm/test/tools/llubi/lib_abort.ll
@@ -23,7 +23,7 @@ entry:
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %before = alloca [7 x i8], align 1 => ptr 0x8 [before]
 ; CHECK-NEXT:   store [7 x i8] c"Before\00", ptr %before, align 1
-; CHECK-NEXT:   %after = alloca [6 x i8], align 1 => ptr 0xF [after]
+; CHECK-NEXT:   %after = alloca [6 x i8], align 1 => ptr 0x10 [after]
 ; CHECK-NEXT:   store [6 x i8] c"After\00", ptr %after, align 1
 ; CHECK-NEXT: Before
 ; CHECK-NEXT:   %0 = call i32 @puts(ptr %before) => i32 1
diff --git a/llvm/test/tools/llubi/lib_exit.ll b/llvm/test/tools/llubi/lib_exit.ll
index 7239352f18182..f5b0e923fcf44 100644
--- a/llvm/test/tools/llubi/lib_exit.ll
+++ b/llvm/test/tools/llubi/lib_exit.ll
@@ -23,7 +23,7 @@ entry:
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %before = alloca [7 x i8], align 1 => ptr 0x8 [before]
 ; CHECK-NEXT:   store [7 x i8] c"Before\00", ptr %before, align 1
-; CHECK-NEXT:   %after = alloca [6 x i8], align 1 => ptr 0xF [after]
+; CHECK-NEXT:   %after = alloca [6 x i8], align 1 => ptr 0x10 [after]
 ; CHECK-NEXT:   store [6 x i8] c"After\00", ptr %after, align 1
 ; CHECK-NEXT: Before
 ; CHECK-NEXT:   %0 = call i32 @puts(ptr %before) => i32 1
diff --git a/llvm/test/tools/llubi/lib_io.ll b/llvm/test/tools/llubi/lib_io.ll
index 157d11942a64c..d6f71c61d73d5 100644
--- a/llvm/test/tools/llubi/lib_io.ll
+++ b/llvm/test/tools/llubi/lib_io.ll
@@ -26,9 +26,9 @@ entry:
 ; CHECK-NEXT:   store [13 x i8] c"Hello, puts!\00", ptr %puts.str, align 1
 ; CHECK-NEXT: Hello, puts!
 ; CHECK-NEXT:   %0 = call i32 @puts(ptr %puts.str) => i32 1
-; CHECK-NEXT:   %fmt.str = alloca [18 x i8], align 1 => ptr 0x15 [fmt.str]
+; CHECK-NEXT:   %fmt.str = alloca [18 x i8], align 1 => ptr 0x16 [fmt.str]
 ; CHECK-NEXT:   store [18 x i8] c"Int: %d, Str: %s\0A\00", ptr %fmt.str, align 1
-; CHECK-NEXT:   %arg.str = alloca [5 x i8], align 1 => ptr 0x27 [arg.str]
+; CHECK-NEXT:   %arg.str = alloca [5 x i8], align 1 => ptr 0x29 [arg.str]
 ; CHECK-NEXT:   store [5 x i8] c"test\00", ptr %arg.str, align 1
 ; CHECK-NEXT: Int: 42, Str: test
 ; CHECK-NEXT:   %1 = call i32 (ptr, ...) @printf(ptr %fmt.str, i32 42, ptr %arg.str) => i32 19
diff --git a/llvm/test/tools/llubi/lib_printf_format.ll b/llvm/test/tools/llubi/lib_printf_format.ll
index e4d5dea1b6a27..b14b0f618f4a0 100644
--- a/llvm/test/tools/llubi/lib_printf_format.ll
+++ b/llvm/test/tools/llubi/lib_printf_format.ll
@@ -46,27 +46,27 @@ entry:
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %fmt_int = alloca [36 x i8], align 1 => ptr 0x8 [fmt_int]
 ; CHECK-NEXT:   store [36 x i8] c"Ints: %d, %i, %u, %o, %x, %X, %05d\0A\00", ptr %fmt_int, align 1
-; CHECK-NEXT:   %fmt_len = alloca [35 x i8], align 1 => ptr 0x2C [fmt_len]
+; CHECK-NEXT:   %fmt_len = alloca [35 x i8], align 1 => ptr 0x2D [fmt_len]
 ; CHECK-NEXT:   store [35 x i8] c"Lengths: %ld, %lld, %hd, %hhu, %c\0A\00", ptr %fmt_len, align 1
-; CHECK-NEXT:   %fmt_str_ptr = alloca [18 x i8], align 1 => ptr 0x4F [fmt_str_ptr]
+; CHECK-NEXT:   %fmt_str_ptr = alloca [18 x i8], align 1 => ptr 0x51 [fmt_str_ptr]
 ; CHECK-NEXT:   store [18 x i8] c"Str: %s, Ptr: %p\0A\00", ptr %fmt_str_ptr, align 1
-; CHECK-NEXT:   %fmt_pct = alloca [15 x i8], align 1 => ptr 0x61 [fmt_pct]
+; CHECK-NEXT:   %fmt_pct = alloca [15 x i8], align 1 => ptr 0x64 [fmt_pct]
 ; CHECK-NEXT:   store [15 x i8] c"Percent: %d%%\0A\00", ptr %fmt_pct, align 1
-; CHECK-NEXT:   %dummy_str = alloca [6 x i8], align 1 => ptr 0x70 [dummy_str]
+; CHECK-NEXT:   %dummy_str = alloca [6 x i8], align 1 => ptr 0x74 [dummy_str]
 ; CHECK-NEXT:   store [6 x i8] c"llubi\00", ptr %dummy_str, align 1
-; CHECK-NEXT:   %fmt_float = alloca [20 x i8], align 1 => ptr 0x76 [fmt_float]
+; CHECK-NEXT:   %fmt_float = alloca [20 x i8], align 1 => ptr 0x7B [fmt_float]
 ; CHECK-NEXT:   store [20 x i8] c"Floats: %f, %e, %g\0A\00", ptr %fmt_float, align 1
-; CHECK-NEXT:   %fmt_n = alloca [15 x i8], align 1 => ptr 0x8A [fmt_n]
+; CHECK-NEXT:   %fmt_n = alloca [15 x i8], align 1 => ptr 0x90 [fmt_n]
 ; CHECK-NEXT:   store [15 x i8] c"Count: %nDone\0A\00", ptr %fmt_n, align 1
-; CHECK-NEXT:   %fmt_n_out = alloca [6 x i8], align 1 => ptr 0x99 [fmt_n_out]
+; CHECK-NEXT:   %fmt_n_out = alloca [6 x i8], align 1 => ptr 0xA0 [fmt_n_out]
 ; CHECK-NEXT:   store [6 x i8] c"N=%d\0A\00", ptr %fmt_n_out, align 1
-; CHECK-NEXT:   %n_count = alloca i32, align 4 => ptr 0xA0 [n_count]
+; CHECK-NEXT:   %n_count = alloca i32, align 4 => ptr 0xA8 [n_count]
 ; CHECK-NEXT:   store i32 0, ptr %n_count, align 4
 ; CHECK-NEXT: Ints: 42, -42, 255, 377, ff, FF, 00042
 ; CHECK-NEXT:   %0 = call i32 (ptr, ...) @printf(ptr %fmt_int, i32 42, i32 -42, i32 255, i32 255, i32 255, i32 255, i32 42) => i32 39
 ; CHECK-NEXT: Lengths: 123456789, 987654321, 100, 50, A
 ; CHECK-NEXT:   %1 = call i32 (ptr, ...) @printf(ptr %fmt_len, i64 123456789, i64 987654321, i32 100, i32 50, i32 65) => i32 42
-; CHECK-NEXT: Str: llubi, Ptr: 0x70
+; CHECK-NEXT: Str: llubi, Ptr: 0x74
 ; CHECK-NEXT:   %2 = call i32 (ptr, ...) @printf(ptr %fmt_str_ptr, ptr %dummy_str, ptr %dummy_str) => i32 22
 ; CHECK-NEXT: Percent: 100%
 ; CHECK-NEXT:   %3 = call i32 (ptr, ...) @printf(ptr %fmt_pct, i32 100) => i32 14
diff --git a/llvm/test/tools/llubi/lib_terminate.ll b/llvm/test/tools/llubi/lib_terminate.ll
index 8026e9aa24497..8849d356d34ae 100644
--- a/llvm/test/tools/llubi/lib_terminate.ll
+++ b/llvm/test/tools/llubi/lib_terminate.ll
@@ -23,7 +23,7 @@ entry:
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %before = alloca [7 x i8], align 1 => ptr 0x8 [before]
 ; CHECK-NEXT:   store [7 x i8] c"Before\00", ptr %before, align 1
-; CHECK-NEXT:   %after = alloca [6 x i8], align 1 => ptr 0xF [after]
+; CHECK-NEXT:   %after = alloca [6 x i8], align 1 => ptr 0x10 [after]
 ; CHECK-NEXT:   store [6 x i8] c"After\00", ptr %after, align 1
 ; CHECK-NEXT: Before
 ; CHECK-NEXT:   %0 = call i32 @puts(ptr %before) => i32 1
diff --git a/llvm/test/tools/llubi/loadstore_be.ll b/llvm/test/tools/llubi/loadstore_be.ll
index 8f2e0815f88fe..8f1f851ff13c2 100644
--- a/llvm/test/tools/llubi/loadstore_be.ll
+++ b/llvm/test/tools/llubi/loadstore_be.ll
@@ -146,7 +146,7 @@ define void @main() {
 ; CHECK-NEXT:   %val11 = load i25, ptr %alloc, align 4 => poison
 ; CHECK-NEXT:   call void @llvm.lifetime.start.p0(ptr poison)
 ; CHECK-NEXT:   call void @llvm.lifetime.end.p0(ptr poison)
-; CHECK-NEXT:   %alloc_lifetime = alloca i32, align 4 => ptr 0xC [alloc_lifetime (dead)]
+; CHECK-NEXT:   %alloc_lifetime = alloca i32, align 4 => ptr 0x10 [alloc_lifetime (dead)]
 ; CHECK-NEXT:   %val12 = load i32, ptr %alloc_lifetime, align 4 => poison
 ; CHECK-NEXT:   call void @llvm.lifetime.start.p0(ptr %alloc_lifetime)
 ; CHECK-NEXT:   %val13 = load i32, ptr %alloc_lifetime, align 4 => i32 -289830082
@@ -161,52 +161,52 @@ define void @main() {
 ; CHECK-NEXT:   %val16 = load i32, ptr %alloc_lifetime, align 4 => poison
 ; CHECK-NEXT:   store i32 -524288, ptr %alloc, align 4
 ; CHECK-NEXT:   %val17 = load float, ptr %alloc, align 4 => float 0xFFF80000
-; CHECK-NEXT:   %alloc_vscale = alloca <vscale x 2 x i32>, align 8 => ptr 0x10 [alloc_vscale]
+; CHECK-NEXT:   %alloc_vscale = alloca <vscale x 2 x i32>, align 8 => ptr 0x18 [alloc_vscale]
 ; CHECK-NEXT:   %insert = insertelement <vscale x 1 x i32> poison, i32 1, i32 0 => { i32 1, poison, poison, poison }
 ; CHECK-NEXT:   %ones = shufflevector <vscale x 1 x i32> %insert, <vscale x 1 x i32> poison, <vscale x 1 x i32> zeroinitializer => { i32 1, i32 1, i32 1, i32 1 }
 ; CHECK-NEXT:   %twos = add <vscale x 1 x i32> %ones, %ones => { i32 2, i32 2, i32 2, i32 2 }
 ; CHECK-NEXT:   store <vscale x 1 x i32> %ones, ptr %alloc_vscale, align 4
-; CHECK-NEXT:   %gep3 = getelementptr <vscale x 1 x i32>, ptr %alloc_vscale, i64 1 => ptr 0x20 [alloc_vscale + 16]
+; CHECK-NEXT:   %gep3 = getelementptr <vscale x 1 x i32>, ptr %alloc_vscale, i64 1 => ptr 0x28 [alloc_vscale + 16]
 ; CHECK-NEXT:   store <vscale x 1 x i32> %twos, ptr %gep3, align 4
 ; CHECK-NEXT:   %val18 = load <vscale x 2 x i32>, ptr %alloc_vscale, align 8 => { i32 1, i32 1, i32 1, i32 1, i32 2, i32 2, i32 2, i32 2 }
-; CHECK-NEXT:   %alloc_struct = alloca %struct, align 8 => ptr 0x30 [alloc_struct]
+; CHECK-NEXT:   %alloc_struct = alloca %struct, align 8 => ptr 0x40 [alloc_struct]
 ; CHECK-NEXT:   store %struct { [2 x i16] [i16 1, i16 2], i64 3 }, ptr %alloc_struct, align 8
 ; CHECK-NEXT:   %val19 = load %struct, ptr %alloc_struct, align 8 => { { i16 1, i16 2 }, i64 3 }
 ; CHECK-NEXT:   %val20 = load i64, ptr %alloc_struct, align 8 => i64 281486191577587
 ; CHECK-NEXT:   %val21 = load i64, ptr %alloc_struct, align 8 => i64 281486962127119
-; CHECK-NEXT:   %alloc_struct_packed = alloca %struct.packed, align 8 => ptr 0x40 [alloc_struct_packed]
+; CHECK-NEXT:   %alloc_struct_packed = alloca %struct.packed, align 8 => ptr 0x58 [alloc_struct_packed]
 ; CHECK-NEXT:   store %struct.packed <{ [2 x i16] [i16 1, i16 2], i64 3 }>, ptr %alloc_struct_packed, align 1
 ; CHECK-NEXT:   %val22 = load %struct.packed, ptr %alloc_struct_packed, align 1 => { { i16 1, i16 2 }, i64 3 }
 ; CHECK-NEXT:   %val23 = load i64, ptr %alloc_struct_packed, align 8 => i64 281483566645248
 ; CHECK-NEXT:   %val24 = load i64, ptr %alloc_struct_packed, align 8 => i64 281483566645248
-; CHECK-NEXT:   %alloc_struct_vscale = alloca %struct.vscale, align 8 => ptr 0x50 [alloc_struct_vscale]
+; CHECK-NEXT:   %alloc_struct_vscale = alloca %struct.vscale, align 8 => ptr 0x68 [alloc_struct_vscale]
 ; CHECK-NEXT:   store %struct.vscale zeroinitializer, ptr %alloc_struct_vscale, align 4
-; CHECK-NEXT:   %gep4 = getelementptr <vscale x 1 x i32>, ptr %alloc_struct_vscale, i32 1 => ptr 0x60 [alloc_struct_vscale + 16]
+; CHECK-NEXT:   %gep4 = getelementptr <vscale x 1 x i32>, ptr %alloc_struct_vscale, i32 1 => ptr 0x78 [alloc_struct_vscale + 16]
 ; CHECK-NEXT:   store <vscale x 1 x i32> %ones, ptr %gep4, align 4
 ; CHECK-NEXT:   %val25 = load %struct.vscale, ptr %alloc_struct_vscale, align 4 => { { i32 0, i32 0, i32 0, i32 0 }, { i32 1, i32 1, i32 1, i32 1 } }
-; CHECK-NEXT:   %alloc_array = alloca [2 x i32], align 4 => ptr 0x70 [alloc_array]
+; CHECK-NEXT:   %alloc_array = alloca [2 x i32], align 4 => ptr 0x8C [alloc_array]
 ; CHECK-NEXT:   store [2 x i32] [i32 1, i32 2], ptr %alloc_array, align 4
 ; CHECK-NEXT:   %val26 = load [2 x i32], ptr %alloc_array, align 4 => { i32 1, i32 2 }
-; CHECK-NEXT:   %alloc_i1_vec = alloca <4 x i1>, align 1 => ptr 0x78 [alloc_i1_vec]
+; CHECK-NEXT:   %alloc_i1_vec = alloca <4 x i1>, align 1 => ptr 0x95 [alloc_i1_vec]
 ; CHECK-NEXT:   store <4 x i1> <i1 true, i1 false, i1 poison, i1 false>, ptr %alloc_i1_vec, align 1
 ; CHECK-NEXT:   %val27 = load <4 x i1>, ptr %alloc_i1_vec, align 1 => { T, F, poison, F }
 ; CHECK-NEXT:   %val28 = load i8, ptr %alloc_i1_vec, align 1 => poison
-; CHECK-NEXT:   %alloc_padding = alloca i31, align 4 => ptr 0x7C [alloc_padding]
+; CHECK-NEXT:   %alloc_padding = alloca i31, align 4 => ptr 0x98 [alloc_padding]
 ; CHECK-NEXT:   store i32 0, ptr %alloc_padding, align 4
-; CHECK-NEXT:   %alloc_padding_vec = alloca i64, align 8 => ptr 0x80 [alloc_padding_vec]
+; CHECK-NEXT:   %alloc_padding_vec = alloca i64, align 8 => ptr 0xA0 [alloc_padding_vec]
 ; CHECK-NEXT:   store { <6 x i5>, i32 } { <6 x i5> zeroinitializer, i32 -1 }, ptr %alloc_padding_vec, align 4
 ; CHECK-NEXT:   %load_agg = load { <6 x i5>, i32 }, ptr %alloc_padding_vec, align 4 => { { i5 0, i5 0, i5 0, i5 0, i5 0, i5 0 }, i32 -1 }
 ; 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:   %alloc_struct_padding = alloca { i8, i32 }, align 8 => ptr 0xB0 [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:   %alloc_ptr = alloca ptr, align 8 => ptr 0x90 [alloc_ptr]
+; CHECK-NEXT:   %alloc_ptr = alloca ptr, align 8 => ptr 0xC0 [alloc_ptr]
 ; CHECK-NEXT:   store ptr %alloc_ptr, ptr %alloc_ptr, align 8
-; CHECK-NEXT:   %ptr_with_provenance = load ptr, ptr %alloc_ptr, align 8 => ptr 0x90 [alloc_ptr]
+; CHECK-NEXT:   %ptr_with_provenance = load ptr, ptr %alloc_ptr, align 8 => ptr 0xC0 [alloc_ptr]
 ; CHECK-NEXT:   %addr_bits = load i8, ptr %alloc_ptr, align 1 => i8 0
 ; CHECK-NEXT:   store i8 %addr_bits, ptr %alloc_ptr, align 1
-; CHECK-NEXT:   %ptr_without_provenance = load ptr, ptr %alloc_ptr, align 8 => ptr 0x90 [nullary]
+; CHECK-NEXT:   %ptr_without_provenance = load ptr, ptr %alloc_ptr, align 8 => ptr 0xC0 [nullary]
 ; 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 3caf02c3e6eaa..9ad92ac6e75e0 100644
--- a/llvm/test/tools/llubi/loadstore_le.ll
+++ b/llvm/test/tools/llubi/loadstore_le.ll
@@ -148,7 +148,7 @@ define void @main() {
 ; CHECK-NEXT:   %val11 = load i25, ptr %alloc, align 4 => poison
 ; CHECK-NEXT:   call void @llvm.lifetime.start.p0(ptr poison)
 ; CHECK-NEXT:   call void @llvm.lifetime.end.p0(ptr poison)
-; CHECK-NEXT:   %alloc_lifetime = alloca i32, align 4 => ptr 0xC [alloc_lifetime (dead)]
+; CHECK-NEXT:   %alloc_lifetime = alloca i32, align 4 => ptr 0x10 [alloc_lifetime (dead)]
 ; CHECK-NEXT:   %val12 = load i32, ptr %alloc_lifetime, align 4 => poison
 ; CHECK-NEXT:   call void @llvm.lifetime.start.p0(ptr %alloc_lifetime)
 ; CHECK-NEXT:   %val13 = load i32, ptr %alloc_lifetime, align 4 => i32 -289830082
@@ -163,52 +163,52 @@ define void @main() {
 ; CHECK-NEXT:   %val16 = load i32, ptr %alloc_lifetime, align 4 => poison
 ; CHECK-NEXT:   store i32 -524288, ptr %alloc, align 4
 ; CHECK-NEXT:   %val17 = load float, ptr %alloc, align 4 => float 0xFFF80000
-; CHECK-NEXT:   %alloc_vscale = alloca <vscale x 2 x i32>, align 8 => ptr 0x10 [alloc_vscale]
+; CHECK-NEXT:   %alloc_vscale = alloca <vscale x 2 x i32>, align 8 => ptr 0x18 [alloc_vscale]
 ; CHECK-NEXT:   %insert = insertelement <vscale x 1 x i32> poison, i32 1, i32 0 => { i32 1, poison, poison, poison }
 ; CHECK-NEXT:   %ones = shufflevector <vscale x 1 x i32> %insert, <vscale x 1 x i32> poison, <vscale x 1 x i32> zeroinitializer => { i32 1, i32 1, i32 1, i32 1 }
 ; CHECK-NEXT:   %twos = add <vscale x 1 x i32> %ones, %ones => { i32 2, i32 2, i32 2, i32 2 }
 ; CHECK-NEXT:   store <vscale x 1 x i32> %ones, ptr %alloc_vscale, align 4
-; CHECK-NEXT:   %gep3 = getelementptr <vscale x 1 x i32>, ptr %alloc_vscale, i64 1 => ptr 0x20 [alloc_vscale + 16]
+; CHECK-NEXT:   %gep3 = getelementptr <vscale x 1 x i32>, ptr %alloc_vscale, i64 1 => ptr 0x28 [alloc_vscale + 16]
 ; CHECK-NEXT:   store <vscale x 1 x i32> %twos, ptr %gep3, align 4
 ; CHECK-NEXT:   %val18 = load <vscale x 2 x i32>, ptr %alloc_vscale, align 8 => { i32 1, i32 1, i32 1, i32 1, i32 2, i32 2, i32 2, i32 2 }
-; CHECK-NEXT:   %alloc_struct = alloca %struct, align 8 => ptr 0x30 [alloc_struct]
+; CHECK-NEXT:   %alloc_struct = alloca %struct, align 8 => ptr 0x40 [alloc_struct]
 ; CHECK-NEXT:   store %struct { [2 x i16] [i16 1, i16 2], i64 3 }, ptr %alloc_struct, align 8
 ; CHECK-NEXT:   %val19 = load %struct, ptr %alloc_struct, align 8 => { { i16 1, i16 2 }, i64 3 }
 ; CHECK-NEXT:   %val20 = load i64, ptr %alloc_struct, align 8 => i64 -7172745523491635199
 ; CHECK-NEXT:   %val21 = load i64, ptr %alloc_struct, align 8 => i64 -3863260483603529727
-; CHECK-NEXT:   %alloc_struct_packed = alloca %struct.packed, align 8 => ptr 0x40 [alloc_struct_packed]
+; CHECK-NEXT:   %alloc_struct_packed = alloca %struct.packed, align 8 => ptr 0x58 [alloc_struct_packed]
 ; CHECK-NEXT:   store %struct.packed <{ [2 x i16] [i16 1, i16 2], i64 3 }>, ptr %alloc_struct_packed, align 1
 ; CHECK-NEXT:   %val22 = load %struct.packed, ptr %alloc_struct_packed, align 1 => { { i16 1, i16 2 }, i64 3 }
 ; CHECK-NEXT:   %val23 = load i64, ptr %alloc_struct_packed, align 8 => i64 12885032961
 ; CHECK-NEXT:   %val24 = load i64, ptr %alloc_struct_packed, align 8 => i64 12885032961
-; CHECK-NEXT:   %alloc_struct_vscale = alloca %struct.vscale, align 8 => ptr 0x50 [alloc_struct_vscale]
+; CHECK-NEXT:   %alloc_struct_vscale = alloca %struct.vscale, align 8 => ptr 0x68 [alloc_struct_vscale]
 ; CHECK-NEXT:   store %struct.vscale zeroinitializer, ptr %alloc_struct_vscale, align 4
-; CHECK-NEXT:   %gep4 = getelementptr <vscale x 1 x i32>, ptr %alloc_struct_vscale, i32 1 => ptr 0x60 [alloc_struct_vscale + 16]
+; CHECK-NEXT:   %gep4 = getelementptr <vscale x 1 x i32>, ptr %alloc_struct_vscale, i32 1 => ptr 0x78 [alloc_struct_vscale + 16]
 ; CHECK-NEXT:   store <vscale x 1 x i32> %ones, ptr %gep4, align 4
 ; CHECK-NEXT:   %val25 = load %struct.vscale, ptr %alloc_struct_vscale, align 4 => { { i32 0, i32 0, i32 0, i32 0 }, { i32 1, i32 1, i32 1, i32 1 } }
-; CHECK-NEXT:   %alloc_array = alloca [2 x i32], align 4 => ptr 0x70 [alloc_array]
+; CHECK-NEXT:   %alloc_array = alloca [2 x i32], align 4 => ptr 0x8C [alloc_array]
 ; CHECK-NEXT:   store [2 x i32] [i32 1, i32 2], ptr %alloc_array, align 4
 ; CHECK-NEXT:   %val26 = load [2 x i32], ptr %alloc_array, align 4 => { i32 1, i32 2 }
-; CHECK-NEXT:   %alloc_i1_vec = alloca <4 x i1>, align 1 => ptr 0x78 [alloc_i1_vec]
+; CHECK-NEXT:   %alloc_i1_vec = alloca <4 x i1>, align 1 => ptr 0x95 [alloc_i1_vec]
 ; CHECK-NEXT:   store <4 x i1> <i1 true, i1 false, i1 poison, i1 false>, ptr %alloc_i1_vec, align 1
 ; CHECK-NEXT:   %val27 = load <4 x i1>, ptr %alloc_i1_vec, align 1 => { T, F, poison, F }
 ; CHECK-NEXT:   %val28 = load i8, ptr %alloc_i1_vec, align 1 => poison
-; CHECK-NEXT:   %alloc_padding = alloca i31, align 4 => ptr 0x7C [alloc_padding]
+; CHECK-NEXT:   %alloc_padding = alloca i31, align 4 => ptr 0x98 [alloc_padding]
 ; CHECK-NEXT:   store i32 0, ptr %alloc_padding, align 4
-; CHECK-NEXT:   %alloc_padding_vec = alloca i64, align 8 => ptr 0x80 [alloc_padding_vec]
+; CHECK-NEXT:   %alloc_padding_vec = alloca i64, align 8 => ptr 0xA0 [alloc_padding_vec]
 ; CHECK-NEXT:   store { <6 x i5>, i32 } { <6 x i5> zeroinitializer, i32 -1 }, ptr %alloc_padding_vec, align 4
 ; CHECK-NEXT:   %load_agg = load { <6 x i5>, i32 }, ptr %alloc_padding_vec, align 4 => { { i5 0, i5 0, i5 0, i5 0, i5 0, i5 0 }, i32 -1 }
 ; 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:   %alloc_struct_padding = alloca { i8, i32 }, align 8 => ptr 0xB0 [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:   %alloc_ptr = alloca ptr, align 8 => ptr 0x90 [alloc_ptr]
+; CHECK-NEXT:   %alloc_ptr = alloca ptr, align 8 => ptr 0xC0 [alloc_ptr]
 ; CHECK-NEXT:   store ptr %alloc_ptr, ptr %alloc_ptr, align 8
-; CHECK-NEXT:   %ptr_with_provenance = load ptr, ptr %alloc_ptr, align 8 => ptr 0x90 [alloc_ptr]
-; CHECK-NEXT:   %addr_bits = load i8, ptr %alloc_ptr, align 1 => i8 -112
+; CHECK-NEXT:   %ptr_with_provenance = load ptr, ptr %alloc_ptr, align 8 => ptr 0xC0 [alloc_ptr]
+; CHECK-NEXT:   %addr_bits = load i8, ptr %alloc_ptr, align 1 => i8 -64
 ; CHECK-NEXT:   store i8 %addr_bits, ptr %alloc_ptr, align 1
-; CHECK-NEXT:   %ptr_without_provenance = load ptr, ptr %alloc_ptr, align 8 => ptr 0x90 [nullary]
+; CHECK-NEXT:   %ptr_without_provenance = load ptr, ptr %alloc_ptr, align 8 => ptr 0xC0 [nullary]
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/metadata.ll b/llvm/test/tools/llubi/metadata.ll
index 4c2759698e260..bf971c8ad7ef8 100644
--- a/llvm/test/tools/llubi/metadata.ll
+++ b/llvm/test/tools/llubi/metadata.ll
@@ -56,9 +56,9 @@ define void @main() {
 ; CHECK-NEXT:   store float 0.000000e+00, ptr %alloc, align 4
 ; CHECK-NEXT:   %nofpclass_load_valid = load float, ptr %alloc, align 4, !noundef !1, !nofpclass !4 => float 0.000000e+00
 ; CHECK-NEXT:   %nofpclass_load_invalid = load float, ptr %alloc, align 4, !nofpclass !5 => poison
-; CHECK-NEXT:   %alloc_ptr = alloca ptr, align 8 => ptr 0x40 [alloc_ptr]
+; CHECK-NEXT:   %alloc_ptr = alloca ptr, align 8 => ptr 0x48 [alloc_ptr]
 ; CHECK-NEXT:   store ptr %alloc_ptr, ptr %alloc_ptr, align 8
-; CHECK-NEXT:   %align_nonnull_load_valid = load ptr, ptr %alloc_ptr, align 8, !nonnull !1, !dereferenceable !6, !dereferenceable_or_null !6, !align !6, !noundef !1 => ptr 0x40 [alloc_ptr]
+; CHECK-NEXT:   %align_nonnull_load_valid = load ptr, ptr %alloc_ptr, align 8, !nonnull !1, !dereferenceable !6, !dereferenceable_or_null !6, !align !6, !noundef !1 => ptr 0x48 [alloc_ptr]
 ; CHECK-NEXT:   store ptr null, ptr %alloc_ptr, align 8
 ; CHECK-NEXT:   %align_load_valid = load ptr, ptr %alloc_ptr, align 8, !dereferenceable_or_null !6, !align !6, !noundef !1 => ptr 0x0 [nullary]
 ; CHECK-NEXT:   %nonnull_load_invalid = load ptr, ptr %alloc_ptr, align 8, !nonnull !1 => poison
@@ -79,10 +79,10 @@ define void @main() {
 ; CHECK-NEXT: Exiting function: callee_fp
 ; CHECK-NEXT:   %nofpclass_call_invalid = call float @callee_fp(), !nofpclass !5 => poison
 ; CHECK-NEXT: Entering function: callee_ptr
-; CHECK-NEXT:   ptr %x = ptr 0x40 [alloc_ptr]
+; CHECK-NEXT:   ptr %x = ptr 0x48 [alloc_ptr]
 ; CHECK-NEXT:   ret ptr %x
 ; CHECK-NEXT: Exiting function: callee_ptr
-; CHECK-NEXT:   %nonnull_align_call_valid = call ptr @callee_ptr(ptr %alloc_ptr), !nonnull !1, !align !6, !noundef !1 => ptr 0x40 [alloc_ptr]
+; CHECK-NEXT:   %nonnull_align_call_valid = call ptr @callee_ptr(ptr %alloc_ptr), !nonnull !1, !align !6, !noundef !1 => ptr 0x48 [alloc_ptr]
 ; CHECK-NEXT: Entering function: callee_ptr
 ; CHECK-NEXT:   ptr %x = ptr 0x0 [nullary]
 ; CHECK-NEXT:   ret ptr %x
@@ -94,15 +94,15 @@ define void @main() {
 ; CHECK-NEXT: Exiting function: callee_ptr
 ; CHECK-NEXT:   %nonnull_call_invalid = call ptr @callee_ptr(ptr null), !nonnull !1 => poison
 ; CHECK-NEXT: Entering function: callee_ptr
-; CHECK-NEXT:   ptr %x = ptr 0x40 [alloc_ptr]
+; CHECK-NEXT:   ptr %x = ptr 0x48 [alloc_ptr]
 ; CHECK-NEXT:   ret ptr %x
 ; CHECK-NEXT: Exiting function: callee_ptr
-; CHECK-NEXT:   %dereferenceable_call_valid = call ptr @callee_ptr(ptr %alloc_ptr), !dereferenceable !6 => ptr 0x40 [alloc_ptr]
+; CHECK-NEXT:   %dereferenceable_call_valid = call ptr @callee_ptr(ptr %alloc_ptr), !dereferenceable !6 => ptr 0x48 [alloc_ptr]
 ; CHECK-NEXT: Entering function: callee_ptr
-; CHECK-NEXT:   ptr %x = ptr 0x40 [alloc_ptr]
+; CHECK-NEXT:   ptr %x = ptr 0x48 [alloc_ptr]
 ; CHECK-NEXT:   ret ptr %x
 ; CHECK-NEXT: Exiting function: callee_ptr
-; CHECK-NEXT:   %dereferenceable_or_null_call_valid1 = call ptr @callee_ptr(ptr %alloc_ptr), !dereferenceable_or_null !6 => ptr 0x40 [alloc_ptr]
+; CHECK-NEXT:   %dereferenceable_or_null_call_valid1 = call ptr @callee_ptr(ptr %alloc_ptr), !dereferenceable_or_null !6 => ptr 0x48 [alloc_ptr]
 ; CHECK-NEXT: Entering function: callee_ptr
 ; CHECK-NEXT:   ptr %x = ptr 0x0 [nullary]
 ; CHECK-NEXT:   ret ptr %x
diff --git a/llvm/tools/llubi/lib/Context.cpp b/llvm/tools/llubi/lib/Context.cpp
index dc503a788754a..0bdece99a95cd 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -581,7 +581,9 @@ Context::allocate(uint64_t Size, uint64_t Align, StringRef Name, unsigned AS,
   auto MemObj = makeIntrusiveRefCnt<MemoryObject>(
       AlignedAddr, Size, Name, AS, InitKind, AllocKind, IsIRGlobalValue);
   MemoryObjects[AlignedAddr] = MemObj;
-  AllocationBase = AlignedAddr + AllocateSize;
+  // Extra padding to make sure getWildcardProvenance resolves to at most one
+  // memory object.
+  AllocationBase = AlignedAddr + AllocateSize + 1;
   UsedMem += AllocateSize;
   return MemObj;
 }

>From 59554ccd7e9ad2b8ace1f67098795e1d2cf66463 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Sat, 30 May 2026 10:44:37 +0800
Subject: [PATCH 2/7] [llubi] Add basic inttoptr/ptrtoint support (limited
 provenance set)

---
 llvm/test/tools/llubi/inttoptr.ll     |  8 ++++
 llvm/tools/llubi/lib/Context.cpp      | 58 +++++++++++++++++++++++++++
 llvm/tools/llubi/lib/Context.h        | 31 +++++++++++++-
 llvm/tools/llubi/lib/ExecutorBase.cpp | 46 ++++++++-------------
 llvm/tools/llubi/lib/ExecutorBase.h   |  3 +-
 llvm/tools/llubi/lib/Interpreter.cpp  | 20 +++++++--
 llvm/tools/llubi/lib/Library.cpp      | 10 +----
 llvm/tools/llubi/lib/Value.h          | 24 ++++++++++-
 8 files changed, 154 insertions(+), 46 deletions(-)

diff --git a/llvm/test/tools/llubi/inttoptr.ll b/llvm/test/tools/llubi/inttoptr.ll
index d29d26f69e2ad..99207a5b649b3 100644
--- a/llvm/test/tools/llubi/inttoptr.ll
+++ b/llvm/test/tools/llubi/inttoptr.ll
@@ -7,11 +7,19 @@ define void @main() {
   %ptr1 = inttoptr i64 0 to ptr
   %ptr2 = inttoptr i8 255 to ptr
   %ptr3 = inttoptr i128 -1 to ptr
+  %alloc = alloca i32
+  %off = ptrtoint ptr %alloc to i64
+  %gep = getelementptr i8, ptr %ptr1, i64 %off
+  %cast = inttoptr i64 %off to ptr
   ret void
 }
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %ptr1 = inttoptr i64 0 to ptr => ptr 0x0 [nullary]
 ; CHECK-NEXT:   %ptr2 = inttoptr i8 -1 to ptr => ptr 0xFF [nullary]
 ; CHECK-NEXT:   %ptr3 = inttoptr i128 -1 to ptr => ptr 0xFFFFFFFFFFFFFFFF [nullary]
+; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x8 [alloc]
+; CHECK-NEXT:   %off = ptrtoint ptr %alloc to i64 => i64 8
+; CHECK-NEXT:   %gep = getelementptr i8, ptr %ptr1, i64 %off => ptr 0x8 [nullary]
+; CHECK-NEXT:   %cast = inttoptr i64 %off to ptr => ptr 0x8 [alloc]
 ; 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 0bdece99a95cd..570f25a65896c 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -602,6 +602,7 @@ bool Context::free(const MemoryObject &Obj) {
   for (const APInt &Tag : MutableObj.AssociatedTags)
     TaggedProvenances.erase(Tag);
   MutableObj.AssociatedTags.clear();
+  ExposedProvenances.erase(Address);
 
   MemoryObjects.erase(It);
   return true;
@@ -614,6 +615,63 @@ Pointer Context::deriveFromMemoryObject(IntrusiveRefCntPtr<MemoryObject> Obj) {
                        Obj->getAddress()));
 }
 
+void Context::exposeProvenance(Provenance &Prov) {
+  if (Prov.Wildcard)
+    return;
+  MemoryObject *Obj = Prov.getMemoryObject();
+  if (!Obj)
+    return;
+  uint64_t Address = Obj->getAddress();
+  ExposedProvenanceSet &Set = ExposedProvenances[Address];
+  if (Set.Set.insert(&Prov).second)
+    Set.List.emplace_back(&Prov);
+}
+
+bool Context::checkProvenance(Provenance &Prov,
+                              function_ref<bool(Provenance &)> Check) {
+  if (!Check(Prov))
+    return false;
+  // Early return for concrete provenances.
+  if (!Prov.Wildcard)
+    return true;
+  auto Iter = ExposedProvenances.find(Prov.Wildcard->BaseAddress);
+  // The memory object has been freed.
+  if (Iter == ExposedProvenances.end())
+    return false;
+  auto &List = Iter->second.List;
+  APInt &Mask = Prov.Wildcard->ActiveMask;
+  uint32_t Generation = Mask.getBitWidth();
+  bool Valid = false;
+  for (uint32_t I = 0; I != Generation; ++I) {
+    if (!Mask[I])
+      continue;
+    if (Check(*List[I]))
+      Valid = true;
+    else
+      Mask.clearBit(I);
+  }
+
+  return Valid;
+}
+
+IntrusiveRefCntPtr<Provenance>
+Context::getWildcardProvenance(const APInt &Address, unsigned AS) {
+  uint64_t Addr = Address.getLimitedValue();
+  auto Iter = ExposedProvenances.upper_bound(Addr);
+  if (Iter == ExposedProvenances.begin())
+    return Provenance::nullary();
+  auto &[BaseAddress, Set] = *std::prev(Iter);
+  auto &Obj = MemoryObjects.at(BaseAddress);
+  if (!Obj->inBounds(Address) || Obj->getAddressSpace() != AS)
+    return Provenance::nullary();
+
+  auto Prov = makeIntrusiveRefCnt<Provenance>(Obj);
+  uint32_t Generation = static_cast<uint32_t>(Set.List.size());
+  Prov->Wildcard =
+      makeIntrusiveRefCnt<WildcardProvenance>(BaseAddress, Generation);
+  return Prov;
+}
+
 Function *Context::getTargetFunction(const Pointer &Ptr) {
   if (Ptr.address().getActiveBits() > 64)
     return nullptr;
diff --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index 5daa9e816f145..4042d19dbaf4a 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -237,8 +237,25 @@ class Context {
   // Mapping from tags to provenances. Tags are lazily generated when a
   // pointer is captured by memory.
   DenseMap<APInt, IntrusiveRefCntPtr<Provenance>> TaggedProvenances;
-  // TODO: Maintains a global list of 'exposed' provenances. This is used to
-  // convert an address back to a pointer with a previously exposed provenance.
+  // Maintains a global list of 'exposed' provenances. This is used to convert
+  // an address back to a pointer with a previously exposed provenance. In
+  // theory the provenance is picked from all previously exposed provenances
+  // using angelic non-determinism. Since llubi is just an interpreter, we make
+  // two approximations:
+  //   1. Each address maps to at most one memory object during the execution of
+  //   the program, as AllocationBase increases monotonically.
+  //   2. We maintain the set of exposed provenances. When ptrtoint/addr
+  //   executes,
+  //      the provenance is inserted to the set. When inttoptr executes, it
+  //      yields a pointer with a wildcard provenance. That is, each later use
+  //      will check whether there is an exposed provenance in the snapshot
+  //      allowing the operation. The invalid provenance will be masked out
+  //      after the operation. If we cannot pick one, it is UB.
+  struct ExposedProvenanceSet {
+    SmallVector<IntrusiveRefCntPtr<Provenance>> List;
+    SmallPtrSet<Provenance *, 4> Set;
+  };
+  std::map<uint64_t, ExposedProvenanceSet> ExposedProvenances;
 
   /// Get the tag for the given pointer provenance.
   APInt getTag(uint32_t BitWidth, Provenance &Prov);
@@ -329,6 +346,16 @@ class Context {
   /// Derive a pointer from a memory object with offset 0.
   /// Please use Pointer's interface for further manipulations.
   Pointer deriveFromMemoryObject(IntrusiveRefCntPtr<MemoryObject> Obj);
+  /// Mark this provenance as exposed. It is no-op if it is not associated with
+  /// a memory object or a wildcard provenance.
+  void exposeProvenance(Provenance &Prov);
+  /// A helper to check both concrete and wildcard provenance. Please don't
+  /// report UB inside the \p Check callback due to the existence of wildcard
+  /// provenance.
+  bool checkProvenance(Provenance &Prov,
+                       function_ref<bool(Provenance &)> Check);
+  IntrusiveRefCntPtr<Provenance> getWildcardProvenance(const APInt &Address,
+                                                       unsigned AS);
   /// Convert byte sequence to a value of the given type. Uninitialized bits are
   /// flushed according to the options.
   /// If \p ContainsUndefinedBits is provided, it will be set to true when there
diff --git a/llvm/tools/llubi/lib/ExecutorBase.cpp b/llvm/tools/llubi/lib/ExecutorBase.cpp
index 1433526f18d3a..409e32fa3491d 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.cpp
+++ b/llvm/tools/llubi/lib/ExecutorBase.cpp
@@ -53,11 +53,18 @@ void ExecutorBase::reportErrorString(StringRef Msg) {
   Handler.onError(Msg);
 }
 
-std::optional<uint64_t> ExecutorBase::verifyMemAccess(const MemoryObject &MO,
-                                                      const APInt &Address,
+std::optional<uint64_t> ExecutorBase::verifyMemAccess(const Pointer &Ptr,
                                                       uint64_t AccessSize,
                                                       Align Alignment,
                                                       bool IsStore) {
+  auto &Prov = Ptr.provenance();
+  if (!Prov.getMemoryObject()) {
+    reportImmediateUB()
+        << "Invalid memory access via a pointer with nullary provenance.";
+    return std::nullopt;
+  }
+  auto &MO = *Prov.getMemoryObject();
+  const APInt &Address = Ptr.address();
   // Loading from a stack object outside its lifetime is not undefined
   // behavior and returns a poison value instead. Storing to it is still
   // undefined behavior.
@@ -107,17 +114,11 @@ AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
     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.";
-    return AnyValue::getPoisonValue(Ctx, ValTy);
-  }
   // TODO: pointer capability check
-  if (auto Offset =
-          verifyMemAccess(*MO, PtrVal.address(),
-                          Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
-                          /*IsStore=*/false)) {
+  if (auto Offset = verifyMemAccess(
+          PtrVal, Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
+          /*IsStore=*/false)) {
+    auto *MO = PtrVal.getMemoryObject();
     // Load from a dead stack object yields poison value.
     if (MO->getState() == MemoryObjectState::Dead)
       return AnyValue::getPoisonValue(Ctx, ValTy);
@@ -139,23 +140,10 @@ void ExecutorBase::store(const AnyValue &Ptr, Align Alignment,
     return;
   }
   auto &PtrVal = Ptr.asPointer();
-  auto *MO = PtrVal.getMemoryObject();
-  if (!MO) {
-    reportImmediateUB()
-        << "Invalid memory access via a pointer with nullary provenance.";
-    return;
-  }
-  if (MO->isConstant()) {
-    reportImmediateUB() << "Try to write to a constant memory object: "
-                        << PtrVal << ".";
-    return;
-  }
-  // TODO: pointer capability check
-  if (auto Offset =
-          verifyMemAccess(*MO, PtrVal.address(),
-                          Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
-                          /*IsStore=*/true))
-    Ctx.store(*MO, *Offset, Val, ValTy);
+  if (auto Offset = verifyMemAccess(
+          PtrVal, Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
+          /*IsStore=*/true))
+    Ctx.store(*PtrVal.getMemoryObject(), *Offset, Val, ValTy);
 }
 
 void ExecutorBase::requestProgramExit(ProgramExitInfo::ProgramExitKind Kind,
diff --git a/llvm/tools/llubi/lib/ExecutorBase.h b/llvm/tools/llubi/lib/ExecutorBase.h
index d07b4dae1792e..78f4696af8628 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.h
+++ b/llvm/tools/llubi/lib/ExecutorBase.h
@@ -101,8 +101,7 @@ class ExecutorBase {
 
   /// Check if the upcoming memory access is valid. Returns the offset relative
   /// to the underlying object if it is valid.
-  std::optional<uint64_t> verifyMemAccess(const MemoryObject &MO,
-                                          const APInt &Address,
+  std::optional<uint64_t> verifyMemAccess(const Pointer &Ptr,
                                           uint64_t AccessSize, Align Alignment,
                                           bool IsStore);
 
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 81cc968535a2f..9cd4cc972e2f5 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -2417,14 +2417,28 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     setResult(GEP, std::move(Res));
   }
 
+  void visitPtrToInt(PtrToIntInst &I) {
+    return visitUnOp(I, [&](const AnyValue &V) -> AnyValue {
+      if (V.isPoison())
+        return AnyValue::poison();
+      Ctx.exposeProvenance(V.asPointer().provenance());
+      return V.asPointer().address();
+    });
+  }
+
+  // TODO: Add support for ptrtoaddr, which only exposes the captured address
+  // capability.
+
   void visitIntToPtr(IntToPtrInst &I) {
+    unsigned AS = I.getType()->getPointerAddressSpace();
     return visitUnOp(I, [&](const AnyValue &V) -> AnyValue {
       if (V.isPoison())
         return AnyValue::poison();
-      // TODO: expose provenance
+      auto Prov = Ctx.getWildcardProvenance(V.asInteger(), AS);
       // TODO: check metadata
-      return Pointer(V.asInteger().zextOrTrunc(
-          DL.getPointerSizeInBits(I.getType()->getPointerAddressSpace())));
+      return Pointer(std::move(Prov),
+                     V.asInteger().zextOrTrunc(DL.getPointerSizeInBits(
+                         I.getType()->getPointerAddressSpace())));
     });
   }
 
diff --git a/llvm/tools/llubi/lib/Library.cpp b/llvm/tools/llubi/lib/Library.cpp
index 3eab215f933d1..15e405ac5a579 100644
--- a/llvm/tools/llubi/lib/Library.cpp
+++ b/llvm/tools/llubi/lib/Library.cpp
@@ -30,19 +30,13 @@ 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.";
-    return std::nullopt;
-  }
-
   std::string Result;
   const APInt &Address = Ptr.address();
   uint64_t Offset = 0;
 
   while (true) {
-    auto ValidOffset =
-        Executor.verifyMemAccess(*MO, Address + Offset, 1, Align(1), false);
+    auto ValidOffset = Executor.verifyMemAccess(
+        Ptr.getWithNewAddr(Address + Offset), 1, Align(1), false);
     if (!ValidOffset)
       return std::nullopt;
 
diff --git a/llvm/tools/llubi/lib/Value.h b/llvm/tools/llubi/lib/Value.h
index 71ee212b4299e..41f7d789e44e2 100644
--- a/llvm/tools/llubi/lib/Value.h
+++ b/llvm/tools/llubi/lib/Value.h
@@ -104,6 +104,25 @@ enum class StorageKind {
 /// Tri-state boolean value.
 enum class BooleanKind { False, True, Poison };
 
+/// A set of previously exposed provenance. It is originally yielded by
+/// inttoptr, and shared by pointers derived from the result.
+class WildcardProvenance : public RefCountedBase<WildcardProvenance> {
+  // Each capability check may invalidate some provenances. If we cannot
+  // pick one, it is UB. That is, from the angelic non-determinism view,
+  // we cannot pick a provenance to make the program reach this point.
+  // The bitwidth represents the generation of the set. We only consider
+  // the exposed provenances in the snapshot, which is captured at the
+  // point where inttoptr executes.
+  APInt ActiveMask;
+  uint64_t BaseAddress;
+
+  friend class Context;
+
+public:
+  WildcardProvenance(uint64_t BaseAddress, uint32_t Generation)
+      : ActiveMask(APInt::getAllOnes(Generation)), BaseAddress(BaseAddress) {}
+};
+
 /// Components of a pointer excluding address. They are shared between pointer
 /// values, as most of operations don't change the provenance.
 /// Each node will be assigned a unique, pointer-sized tag, which is used to
@@ -120,10 +139,11 @@ class Provenance : public RefCountedBase<Provenance> {
   // type, in bits. It may produce false negatives in some corner cases. But in
   // real practice the false negative rate should be negligible.
   // A zero tag is invalid.
-  // TODO: we need a special tag for wildcard provenance, which is introduced by
-  // inttoptr.
   APInt Tag;
 
+  // Null if it is concrete.
+  IntrusiveRefCntPtr<WildcardProvenance> Wildcard;
+
   // TODO: modeling nofree
   // TODO: modeling captures
   // TODO: modeling inrange(Start, End) attribute

>From 9a0bbf898f51aadecc270ea41281f72705cf956f Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Sun, 31 May 2026 01:40:01 +0800
Subject: [PATCH 3/7] [llubi] Use Miri's semantic

---
 llvm/test/tools/llubi/inttoptr.ll     |  2 +-
 llvm/tools/llubi/lib/Context.cpp      | 91 ++++++++++++++++++---------
 llvm/tools/llubi/lib/Context.h        | 22 +++++--
 llvm/tools/llubi/lib/ExecutorBase.cpp | 69 ++++++++++----------
 llvm/tools/llubi/lib/ExecutorBase.h   | 18 +++---
 llvm/tools/llubi/lib/Interpreter.cpp  | 81 +++++++++++++++---------
 llvm/tools/llubi/lib/Library.cpp      | 20 +++---
 llvm/tools/llubi/lib/Value.cpp        | 14 ++++-
 llvm/tools/llubi/lib/Value.h          | 30 +++++++--
 9 files changed, 228 insertions(+), 119 deletions(-)

diff --git a/llvm/test/tools/llubi/inttoptr.ll b/llvm/test/tools/llubi/inttoptr.ll
index 99207a5b649b3..53310b84af1e9 100644
--- a/llvm/test/tools/llubi/inttoptr.ll
+++ b/llvm/test/tools/llubi/inttoptr.ll
@@ -20,6 +20,6 @@ define void @main() {
 ; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x8 [alloc]
 ; CHECK-NEXT:   %off = ptrtoint ptr %alloc to i64 => i64 8
 ; CHECK-NEXT:   %gep = getelementptr i8, ptr %ptr1, i64 %off => ptr 0x8 [nullary]
-; CHECK-NEXT:   %cast = inttoptr i64 %off to ptr => ptr 0x8 [alloc]
+; CHECK-NEXT:   %cast = inttoptr i64 %off to ptr => ptr 0x8 [wildcard]
 ; 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 570f25a65896c..1eb7160dc07e1 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -623,52 +623,85 @@ void Context::exposeProvenance(Provenance &Prov) {
     return;
   uint64_t Address = Obj->getAddress();
   ExposedProvenanceSet &Set = ExposedProvenances[Address];
-  if (Set.Set.insert(&Prov).second)
+  if (Set.Set.insert(&Prov).second) {
     Set.List.emplace_back(&Prov);
+    Set.GenerationList.push_back(++ExposedProvenanceSetGeneration);
+  }
 }
 
-bool Context::checkProvenance(Provenance &Prov,
-                              function_ref<bool(Provenance &)> Check) {
+MemoryObject *
+Context::checkProvenance(const Pointer &Ptr,
+                         function_ref<bool(const Provenance &)> Check,
+                         unsigned AS, bool HasSideEffect) {
+  auto &Prov = Ptr.provenance();
   if (!Check(Prov))
-    return false;
+    return nullptr;
   // Early return for concrete provenances.
   if (!Prov.Wildcard)
-    return true;
-  auto Iter = ExposedProvenances.find(Prov.Wildcard->BaseAddress);
-  // The memory object has been freed.
-  if (Iter == ExposedProvenances.end())
-    return false;
-  auto &List = Iter->second.List;
-  APInt &Mask = Prov.Wildcard->ActiveMask;
-  uint32_t Generation = Mask.getBitWidth();
+    return Prov.Obj.get();
+
+  MemoryObject *MO = nullptr;
+  APInt TempMask;
+  APInt *Mask = HasSideEffect ? &Prov.Wildcard->ActiveMask : &TempMask;
+  SmallVector<IntrusiveRefCntPtr<Provenance>> *List = nullptr;
+  if (Prov.Wildcard->ActiveMask.isZero()) {
+    // The memory object hasn't been determined.
+    uint64_t Addr = Ptr.address().getLimitedValue();
+    auto Iter = ExposedProvenances.upper_bound(Addr);
+    if (Iter == ExposedProvenances.begin())
+      return nullptr;
+    auto &[BaseAddress, Set] = *std::prev(Iter);
+    auto &Obj = MemoryObjects.at(BaseAddress);
+    if (Obj->getAddressSpace() != AS || !Obj->inBounds(Ptr.address()))
+      return nullptr;
+    MO = Obj.get();
+    uint32_t Generation = std::distance(
+        Set.GenerationList.begin(),
+        upper_bound(Set.GenerationList, Prov.Wildcard->Generation));
+    *Mask = APInt::getAllOnes(Generation);
+    List = &Set.List;
+  } else {
+    // We already determined the memory object in a previous memory access。
+    uint64_t BaseAddress = Prov.Wildcard->BaseAddress;
+    auto Iter = ExposedProvenances.find(BaseAddress);
+    // The memory object has been freed.
+    if (Iter == ExposedProvenances.end())
+      return nullptr;
+    MO = MemoryObjects.at(BaseAddress).get();
+    if (MO->getAddressSpace() != AS || !MO->inBounds(Ptr.address()))
+      return nullptr;
+    if (HasSideEffect)
+      TempMask = Prov.Wildcard->ActiveMask;
+    List = &Iter->second.List;
+  }
+  if (Prov.Obj) {
+    // We already determined the memory object via speculatable operations like
+    // gep inbounds.
+    if (Prov.Obj.get() != MO)
+      return nullptr;
+  }
+
+  uint32_t Generation = Mask->getBitWidth();
   bool Valid = false;
   for (uint32_t I = 0; I != Generation; ++I) {
-    if (!Mask[I])
+    if (!(*Mask)[I])
       continue;
-    if (Check(*List[I]))
+    if (Check(*(*List)[I]))
       Valid = true;
     else
-      Mask.clearBit(I);
+      Mask->clearBit(I);
   }
 
-  return Valid;
+  return Valid ? MO : nullptr;
 }
 
-IntrusiveRefCntPtr<Provenance>
-Context::getWildcardProvenance(const APInt &Address, unsigned AS) {
-  uint64_t Addr = Address.getLimitedValue();
-  auto Iter = ExposedProvenances.upper_bound(Addr);
-  if (Iter == ExposedProvenances.begin())
+IntrusiveRefCntPtr<Provenance> Context::getWildcardProvenance() {
+  // No exposed provenances.
+  if (ExposedProvenanceSetGeneration == 0)
     return Provenance::nullary();
-  auto &[BaseAddress, Set] = *std::prev(Iter);
-  auto &Obj = MemoryObjects.at(BaseAddress);
-  if (!Obj->inBounds(Address) || Obj->getAddressSpace() != AS)
-    return Provenance::nullary();
-
-  auto Prov = makeIntrusiveRefCnt<Provenance>(Obj);
-  uint32_t Generation = static_cast<uint32_t>(Set.List.size());
+  auto Prov = makeIntrusiveRefCnt<Provenance>(nullptr);
   Prov->Wildcard =
-      makeIntrusiveRefCnt<WildcardProvenance>(BaseAddress, Generation);
+      makeIntrusiveRefCnt<WildcardProvenance>(ExposedProvenanceSetGeneration);
   return Prov;
 }
 
diff --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index 4042d19dbaf4a..e805f734acf08 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -251,11 +251,19 @@ class Context {
   //      will check whether there is an exposed provenance in the snapshot
   //      allowing the operation. The invalid provenance will be masked out
   //      after the operation. If we cannot pick one, it is UB.
+
+  /// Exposed provenances are grouped by associated memory objects for efficient
+  /// invalidation.
   struct ExposedProvenanceSet {
     SmallVector<IntrusiveRefCntPtr<Provenance>> List;
+    SmallVector<uint64_t> GenerationList;
+    // FIXME: Implement a partial order comparator for provenance instead of
+    // deduplicating by pointers.
     SmallPtrSet<Provenance *, 4> Set;
   };
   std::map<uint64_t, ExposedProvenanceSet> ExposedProvenances;
+  // Global version number for the set of exposed provenances.
+  uint64_t ExposedProvenanceSetGeneration = 0;
 
   /// Get the tag for the given pointer provenance.
   APInt getTag(uint32_t BitWidth, Provenance &Prov);
@@ -352,10 +360,16 @@ class Context {
   /// A helper to check both concrete and wildcard provenance. Please don't
   /// report UB inside the \p Check callback due to the existence of wildcard
   /// provenance.
-  bool checkProvenance(Provenance &Prov,
-                       function_ref<bool(Provenance &)> Check);
-  IntrusiveRefCntPtr<Provenance> getWildcardProvenance(const APInt &Address,
-                                                       unsigned AS);
+  /// Returns the resolved memory object if success. \p Ptr is guaranteed to be
+  /// within the bounds of the returned memory object. But the state is not
+  /// checked, for better diagnostic messages. If \p HasSideEffect is true, some
+  /// invalid provenances will be masked out. Note that in this case the caller
+  /// must report UB when the result is nullptr.
+  MemoryObject *checkProvenance(const Pointer &Ptr,
+                                function_ref<bool(const Provenance &)> Check,
+                                unsigned AS, bool HasSideEffect = true);
+  /// Returns the snapshot of currently exposed provenances.
+  IntrusiveRefCntPtr<Provenance> getWildcardProvenance();
   /// Convert byte sequence to a value of the given type. Uninitialized bits are
   /// flushed according to the options.
   /// If \p ContainsUndefinedBits is provided, it will be set to true when there
diff --git a/llvm/tools/llubi/lib/ExecutorBase.cpp b/llvm/tools/llubi/lib/ExecutorBase.cpp
index 409e32fa3491d..4c0afca7fa69c 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.cpp
+++ b/llvm/tools/llubi/lib/ExecutorBase.cpp
@@ -53,78 +53,82 @@ void ExecutorBase::reportErrorString(StringRef Msg) {
   Handler.onError(Msg);
 }
 
-std::optional<uint64_t> ExecutorBase::verifyMemAccess(const Pointer &Ptr,
-                                                      uint64_t AccessSize,
-                                                      Align Alignment,
-                                                      bool IsStore) {
-  auto &Prov = Ptr.provenance();
-  if (!Prov.getMemoryObject()) {
+std::pair<MemoryObject *, uint64_t>
+ExecutorBase::verifyMemAccess(const Pointer &Ptr, uint64_t AccessSize,
+                              Align Alignment, bool IsStore, unsigned AS) {
+  auto *MO = Ctx.checkProvenance(
+      Ptr,
+      [](const Provenance &) {
+        // TODO: check provenance
+        // TODO: check inrange(S, E)
+        return true;
+      },
+      AS);
+  if (!MO) {
     reportImmediateUB()
         << "Invalid memory access via a pointer with nullary provenance.";
-    return std::nullopt;
+    return {};
   }
-  auto &MO = *Prov.getMemoryObject();
   const APInt &Address = Ptr.address();
   // Loading from a stack object outside its lifetime is not undefined
   // behavior and returns a poison value instead. Storing to it is still
   // undefined behavior.
-  if (IsStore ? MO.getState() != MemoryObjectState::Alive
-              : MO.getState() == MemoryObjectState::Freed) {
+  if (IsStore ? MO->getState() != MemoryObjectState::Alive
+              : MO->getState() == MemoryObjectState::Freed) {
     reportImmediateUB() << "Try to access a dead memory object at address 0x"
                         << Twine::utohexstr(Address.getZExtValue()) << ".";
-    return std::nullopt;
+    return {};
   }
 
   if (Address.countr_zero() < Log2(Alignment)) {
     reportImmediateUB() << "Misaligned memory access. Address: 0x"
                         << Twine::utohexstr(Address.getZExtValue())
                         << ", Required alignment: " << Alignment.value() << ".";
-    return std::nullopt;
+    return {};
   }
 
-  if (AccessSize > MO.getSize() || Address.ult(MO.getAddress())) {
+  if (AccessSize > MO->getSize() || Address.ult(MO->getAddress())) {
     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;
+                        << Twine::utohexstr(MO->getAddress())
+                        << ", Object size: " << MO->getSize() << ".";
+    return {};
   }
 
-  APInt Offset = Address - MO.getAddress();
+  APInt Offset = Address - MO->getAddress();
 
-  if (Offset.ugt(MO.getSize() - AccessSize)) {
+  if (Offset.ugt(MO->getSize() - AccessSize)) {
     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;
+                        << Twine::utohexstr(MO->getAddress())
+                        << ", Object size: " << MO->getSize() << ".";
+    return {};
   }
 
-  return Offset.getZExtValue();
+  return {MO, Offset.getZExtValue()};
 }
 
 AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
-                            bool NoUndef) {
+                            bool NoUndef, unsigned AS) {
   if (Ptr.isPoison()) {
     reportImmediateUB() << "Invalid memory access with a poison pointer.";
     return AnyValue::getPoisonValue(Ctx, ValTy);
   }
   auto &PtrVal = Ptr.asPointer();
-  // TODO: pointer capability check
-  if (auto Offset = verifyMemAccess(
+  if (auto [MO, Offset] = verifyMemAccess(
           PtrVal, Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
-          /*IsStore=*/false)) {
-    auto *MO = PtrVal.getMemoryObject();
+          /*IsStore=*/false, AS);
+      MO) {
     // Load from a dead stack object yields poison value.
     if (MO->getState() == MemoryObjectState::Dead)
       return AnyValue::getPoisonValue(Ctx, ValTy);
 
     bool ContainsUndefinedBits = false;
-    AnyValue Res = Ctx.load(*MO, *Offset, ValTy,
+    AnyValue Res = Ctx.load(*MO, Offset, ValTy,
                             NoUndef ? &ContainsUndefinedBits : nullptr);
     if (NoUndef && ContainsUndefinedBits)
       reportImmediateUB() << "The value loaded contains undefined bits.";
@@ -134,16 +138,17 @@ AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
 }
 
 void ExecutorBase::store(const AnyValue &Ptr, Align Alignment,
-                         const AnyValue &Val, Type *ValTy) {
+                         const AnyValue &Val, Type *ValTy, unsigned AS) {
   if (Ptr.isPoison()) {
     reportImmediateUB() << "Invalid memory access with a poison pointer.";
     return;
   }
   auto &PtrVal = Ptr.asPointer();
-  if (auto Offset = verifyMemAccess(
+  if (auto [MO, Offset] = verifyMemAccess(
           PtrVal, Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
-          /*IsStore=*/true))
-    Ctx.store(*PtrVal.getMemoryObject(), *Offset, Val, ValTy);
+          /*IsStore=*/true, AS);
+      MO)
+    Ctx.store(*MO, Offset, Val, ValTy);
 }
 
 void ExecutorBase::requestProgramExit(ProgramExitInfo::ProgramExitKind Kind,
diff --git a/llvm/tools/llubi/lib/ExecutorBase.h b/llvm/tools/llubi/lib/ExecutorBase.h
index 78f4696af8628..35a37f2b3c80d 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.h
+++ b/llvm/tools/llubi/lib/ExecutorBase.h
@@ -99,16 +99,16 @@ class ExecutorBase {
   DiagnosticReporter reportImmediateUB();
   DiagnosticReporter reportError();
 
-  /// Check if the upcoming memory access is valid. Returns the offset relative
-  /// to the underlying object if it is valid.
-  std::optional<uint64_t> verifyMemAccess(const Pointer &Ptr,
-                                          uint64_t AccessSize, Align Alignment,
-                                          bool IsStore);
-
-  AnyValue load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
-                bool NoUndef);
+  /// Check if the upcoming memory access is valid. Returns the resolved memory
+  /// object and offset if it is valid.
+  std::pair<MemoryObject *, uint64_t>
+  verifyMemAccess(const Pointer &Ptr, uint64_t AccessSize, Align Alignment,
+                  bool IsStore, unsigned AS);
+
+  AnyValue load(const AnyValue &Ptr, Align Alignment, Type *ValTy, bool NoUndef,
+                unsigned AS);
   void store(const AnyValue &Ptr, Align Alignment, const AnyValue &Val,
-             Type *ValTy);
+             Type *ValTy, unsigned AS);
 
   void requestProgramExit(ProgramExitInfo::ProgramExitKind Kind,
                           uint64_t ExitCode = 0);
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 9cd4cc972e2f5..0e66a25564a1f 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -120,23 +120,28 @@ static bool violatesNoUndefAttr(AnyValue &V) {
 /// Assumes V is either a poison or a pointer.
 static bool violatesDereferenceableBytesAttr(const AnyValue &V, uint64_t Bytes,
                                              bool OrNull, unsigned AS,
-                                             const DataLayout &DL) {
+                                             Context &Ctx) {
   if (V.isPoison())
     return true;
 
   auto &Ptr = V.asPointer();
-  if (Ptr.isNullPtr(AS, DL)) {
+  if (Ptr.isNullPtr(AS, Ctx.getDataLayout())) {
     if (OrNull)
       return false;
     return true;
   }
-  auto *MO = Ptr.getMemoryObject();
+
+  auto *MO = Ctx.checkProvenance(
+      Ptr,
+      [&](const Provenance &) {
+        // TODO: check read_provenance
+        // TODO: check nofree for attributes/metadata.
+        return true;
+      },
+      AS);
   if (!MO)
     return true;
 
-  // TODO: check read_provenance
-  // TODO: check nofree for attributes/metadata.
-
   const APInt &PtrAddr = Ptr.address();
   return Bytes > MO->getSize() || PtrAddr.ult(MO->getAddress()) ||
          PtrAddr.ugt(MO->getAddress() + MO->getSize() - Bytes);
@@ -710,7 +715,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   AnyValue computePtrAdd(const Pointer &Ptr, const APInt &Offset,
-                         GEPNoWrapFlags Flags, AnyValue &AccumulatedOffset) {
+                         GEPNoWrapFlags Flags, AnyValue &AccumulatedOffset,
+                         unsigned AS) {
     if (Offset.isZero())
       return Ptr;
     APInt IndexBits = Ptr.address().trunc(Offset.getBitWidth());
@@ -730,9 +736,14 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     APInt NewAddr = Ptr.address();
     NewAddr.insertBits(NewIndex.asInteger(), 0);
 
-    auto *MO = Ptr.getMemoryObject();
-    if (Flags.isInBounds() && (!MO || !MO->inBounds(NewAddr)))
-      return AnyValue::poison();
+    MemoryObject *MO = nullptr;
+    if (Flags.isInBounds()) {
+      MO = Ctx.checkProvenance(
+          Ptr, [](const Provenance &) { return true; }, AS,
+          /*HasSideEffect=*/false);
+      if (!MO || !MO->inBounds(NewAddr))
+        return AnyValue::poison();
+    }
 
     if (!AccumulatedOffset.isPoison()) {
       AccumulatedOffset =
@@ -744,31 +755,39 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
 
     // Should not expose provenance here even if the new address doesn't point
     // to the original object.
-    return Ptr.getWithNewAddr(NewAddr);
+    auto Res = Ptr.getWithNewAddr(NewAddr);
+    if (MO) {
+      auto &Prov = Res.provenance();
+      if (Prov.isWildcard() && !Prov.getMemoryObject())
+        Res = Res.getWithNewProvenance(Prov.getWithKnownMemoryObject(*MO));
+    }
+    return Res;
   }
 
   AnyValue computePtrAdd(const AnyValue &Ptr, const APInt &Offset,
-                         GEPNoWrapFlags Flags, AnyValue &AccumulatedOffset) {
+                         GEPNoWrapFlags Flags, AnyValue &AccumulatedOffset,
+                         unsigned AS) {
     if (Ptr.isPoison())
       return AnyValue::poison();
-    return computePtrAdd(Ptr.asPointer(), Offset, Flags, AccumulatedOffset);
+    return computePtrAdd(Ptr.asPointer(), Offset, Flags, AccumulatedOffset, AS);
   }
 
   AnyValue computeScaledPtrAdd(const AnyValue &Ptr, const AnyValue &Index,
                                const APInt &Scale, GEPNoWrapFlags Flags,
-                               AnyValue &AccumulatedOffset) {
+                               AnyValue &AccumulatedOffset, unsigned AS) {
     if (Ptr.isPoison() || Index.isPoison())
       return AnyValue::poison();
     assert(Ptr.isPointer() && Index.isInteger() && "Unexpected type.");
     if (Scale.isOne())
-      return computePtrAdd(Ptr, Index.asInteger(), Flags, AccumulatedOffset);
+      return computePtrAdd(Ptr, Index.asInteger(), Flags, AccumulatedOffset,
+                           AS);
     auto ScaledOffset =
         mulNoWrap(Index.asInteger(), Scale, Flags.hasNoUnsignedSignedWrap(),
                   Flags.hasNoUnsignedWrap());
     if (ScaledOffset.isPoison())
       return AnyValue::poison();
     return computePtrAdd(Ptr, ScaledOffset.asInteger(), Flags,
-                         AccumulatedOffset);
+                         AccumulatedOffset, AS);
   }
 
   AnyValue canonicalizeIndex(const AnyValue &Idx, unsigned IndexBitWidth,
@@ -983,7 +1002,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
               break;
             if (violatesDereferenceableBytesAttr(
                     WasOn, DereferenceableBytes.getLimitedValue(),
-                    Kind == Attribute::DereferenceableOrNull, AS, DL))
+                    Kind == Attribute::DereferenceableOrNull, AS, Ctx))
               reportImmediateUB() << "The pointer " << WasOn << " violates "
                                   << (Kind == Attribute::DereferenceableOrNull
                                           ? "dereferenceable_or_null("
@@ -1008,7 +1027,9 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
       auto Ptr = Args[0];
       if (Ptr.isPoison())
         return AnyValue();
-      auto *MO = Ptr.asPointer().getMemoryObject();
+      auto *MO = Ctx.checkProvenance(
+          Ptr.asPointer(), [](const Provenance &) { return true; },
+          CB.getOperand(0)->getType()->getPointerAddressSpace());
       assert(MO && "Memory object accessed by lifetime intrinsic should be "
                    "always valid.");
       if (IID == Intrinsic::lifetime_start) {
@@ -1691,7 +1712,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
               std::max(AttrsAtCallSite.getDereferenceableBytes(),
                        AttrsAtCallee.getDereferenceableBytes())) {
         if (violatesDereferenceableBytesAttr(V, DereferenceableBytes,
-                                             /*OrNull=*/false, AS, DL))
+                                             /*OrNull=*/false, AS, Ctx))
           reportImmediateUB()
               << "The value " << V << " violates dereferenceable("
               << DereferenceableBytes << ") attribute.";
@@ -1699,7 +1720,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
                      std::max(AttrsAtCallSite.getDereferenceableOrNullBytes(),
                               AttrsAtCallee.getDereferenceableOrNullBytes())) {
         if (violatesDereferenceableBytesAttr(V, DereferenceableOrNullBytes,
-                                             /*OrNull=*/true, AS, DL))
+                                             /*OrNull=*/true, AS, Ctx))
           reportImmediateUB() << "The value " << V
                               << " violates "
                                  "dereferenceable_or_null("
@@ -1757,7 +1778,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
               I.getMetadata(LLVMContext::MD_dereferenceable)) {
         uint64_t Bytes = ExtractFirstIntOperand(DereferenceableBytes);
         if (violatesDereferenceableBytesAttr(V, Bytes,
-                                             /*OrNull=*/false, AS, DL))
+                                             /*OrNull=*/false, AS, Ctx))
           reportImmediateUB()
               << "The value " << V << " violates !dereferenceable !{i64 "
               << Bytes << "} metadata.";
@@ -1765,7 +1786,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
                      I.getMetadata(LLVMContext::MD_dereferenceable_or_null)) {
         uint64_t Bytes = ExtractFirstIntOperand(DereferenceableOrNullBytes);
         if (violatesDereferenceableBytesAttr(V, Bytes,
-                                             /*OrNull=*/true, AS, DL))
+                                             /*OrNull=*/true, AS, Ctx))
           reportImmediateUB()
               << "The value " << V << " violates !dereferenceable_or_null!{i64 "
               << Bytes << "} metadata.";
@@ -2347,6 +2368,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   void visitGetElementPtrInst(GetElementPtrInst &GEP) {
     uint32_t IndexBitWidth =
         DL.getIndexSizeInBits(GEP.getType()->getPointerAddressSpace());
+    unsigned AS = GEP.getType()->getPointerAddressSpace();
     GEPNoWrapFlags Flags = GEP.getNoWrapFlags();
     AnyValue Res = getValue(GEP.getPointerOperand());
     AnyValue AccumulatedOffset = APInt(IndexBitWidth, 0);
@@ -2365,7 +2387,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
                  AccumulatedOffset.asAggregate()))
           ResElem = computeScaledPtrAdd(
               ResElem, canonicalizeIndex(IndexElem, IndexBitWidth, Flags),
-              Scale, Flags, OffsetElem);
+              Scale, Flags, OffsetElem, AS);
       } else {
         AnyValue CanonicalIndex =
             canonicalizeIndex(Index, IndexBitWidth, Flags);
@@ -2373,10 +2395,10 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
           for (auto &&[ResElem, OffsetElem] :
                zip(Res.asAggregate(), AccumulatedOffset.asAggregate()))
             ResElem = computeScaledPtrAdd(ResElem, CanonicalIndex, Scale, Flags,
-                                          OffsetElem);
+                                          OffsetElem, AS);
         } else {
           Res = computeScaledPtrAdd(Res, CanonicalIndex, Scale, Flags,
-                                    AccumulatedOffset);
+                                    AccumulatedOffset, AS);
         }
       }
     };
@@ -2430,11 +2452,10 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   // capability.
 
   void visitIntToPtr(IntToPtrInst &I) {
-    unsigned AS = I.getType()->getPointerAddressSpace();
     return visitUnOp(I, [&](const AnyValue &V) -> AnyValue {
       if (V.isPoison())
         return AnyValue::poison();
-      auto Prov = Ctx.getWildcardProvenance(V.asInteger(), AS);
+      auto Prov = Ctx.getWildcardProvenance();
       // TODO: check metadata
       return Pointer(std::move(Prov),
                      V.asInteger().zextOrTrunc(DL.getPointerSizeInBits(
@@ -2444,7 +2465,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
 
   void visitLoadInst(LoadInst &LI) {
     auto RetVal = load(getValue(LI.getPointerOperand()), LI.getAlign(),
-                       LI.getType(), LI.hasMetadata(LLVMContext::MD_noundef));
+                       LI.getType(), LI.hasMetadata(LLVMContext::MD_noundef),
+                       LI.getPointerAddressSpace());
     // TODO: track volatile loads
     handleMetadata(LI.getType(), RetVal, LI);
     setResult(LI, std::move(RetVal));
@@ -2455,7 +2477,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     auto &Val = getValue(SI.getValueOperand());
     // TODO: track volatile stores
     // TODO: handle metadata
-    store(Ptr, SI.getAlign(), Val, SI.getValueOperand()->getType());
+    store(Ptr, SI.getAlign(), Val, SI.getValueOperand()->getType(),
+          SI.getPointerAddressSpace());
     if (!hasProgramExited() && !Handler.onInstructionExecuted(SI, AnyValue()))
       setFailed();
   }
diff --git a/llvm/tools/llubi/lib/Library.cpp b/llvm/tools/llubi/lib/Library.cpp
index 15e405ac5a579..4bdafe8db96ab 100644
--- a/llvm/tools/llubi/lib/Library.cpp
+++ b/llvm/tools/llubi/lib/Library.cpp
@@ -29,18 +29,18 @@ Library::Library(Context &Ctx, EventHandler &Handler, const DataLayout &DL,
     : Ctx(Ctx), Handler(Handler), DL(DL), Executor(Executor) {}
 
 std::optional<std::string> Library::readStringFromMemory(const Pointer &Ptr) {
-  auto *MO = Ptr.getMemoryObject();
   std::string Result;
   const APInt &Address = Ptr.address();
   uint64_t Offset = 0;
 
   while (true) {
-    auto ValidOffset = Executor.verifyMemAccess(
-        Ptr.getWithNewAddr(Address + Offset), 1, Align(1), false);
-    if (!ValidOffset)
+    auto [MO, ValidOffset] =
+        Executor.verifyMemAccess(Ptr.getWithNewAddr(Address + Offset), 1,
+                                 Align(1), /*IsStore=*/false, /*AS=*/0);
+    if (!MO)
       return std::nullopt;
 
-    Byte B = (*MO)[*ValidOffset];
+    Byte B = (*MO)[ValidOffset];
     if (B.ConcreteMask != 0xFF) {
       Executor.reportImmediateUB()
           << "Read uninitialized or poison memory while "
@@ -122,7 +122,13 @@ AnyValue Library::executeFree(ArrayRef<AnyValue> Args) {
   if (Ptr.isNullPtr(/*AS=*/0, DL))
     return AnyValue();
 
-  MemoryObject *Obj = Ptr.getMemoryObject();
+  MemoryObject *Obj = Ctx.checkProvenance(
+      Ptr,
+      [](const Provenance &) {
+        // TODO: check nofree
+        return true;
+      },
+      /*AS=*/0);
   if (!Obj) {
     Executor.reportImmediateUB()
         << "freeing a pointer with nullary provenance.";
@@ -276,7 +282,7 @@ AnyValue Library::executePrintf(ArrayRef<AnyValue> Args) {
     case 'n': {
       OS.flush();
       Executor.store(Arg, Align(4), AnyValue(APInt(32, Output.size())),
-                     Type::getInt32Ty(Ctx.getContext()));
+                     Type::getInt32Ty(Ctx.getContext()), /*AS=*/0);
       break;
     }
     case 'p': {
diff --git a/llvm/tools/llubi/lib/Value.cpp b/llvm/tools/llubi/lib/Value.cpp
index 04a1aa803f66c..3f1c2567cc51d 100644
--- a/llvm/tools/llubi/lib/Value.cpp
+++ b/llvm/tools/llubi/lib/Value.cpp
@@ -22,11 +22,20 @@ IntrusiveRefCntPtr<Provenance> Provenance::nullary() {
   return Instance;
 }
 
+IntrusiveRefCntPtr<Provenance>
+Provenance::getWithKnownMemoryObject(MemoryObject &KnownObj) {
+  assert(!Obj && Wildcard && "The memory object has been determined.");
+  auto Res = makeIntrusiveRefCnt<Provenance>(*this);
+  Res->Obj = &KnownObj;
+  Res->Tag = APInt();
+  return Res;
+}
+
 void Pointer::print(raw_ostream &OS) const {
   SmallString<32> AddrStr;
   Address.toStringUnsigned(AddrStr, 16);
   OS << "ptr 0x" << AddrStr << " [";
-  if (MemoryObject *Obj = getMemoryObject()) {
+  if (MemoryObject *Obj = Prov->getMemoryObject()) {
     if (Obj->isIRGlobalValue())
       OS << "@";
     OS << Obj->getName();
@@ -36,8 +45,9 @@ void Pointer::print(raw_ostream &OS) const {
     if (State != MemoryObjectState::Alive)
       OS << (State == MemoryObjectState::Dead ? " (dead)" : " (dangling)");
   } else {
-    OS << "nullary";
+    OS << (Prov->isWildcard() ? "wildcard" : "nullary");
   }
+  // TODO: print provenance
   OS << "]";
 }
 
diff --git a/llvm/tools/llubi/lib/Value.h b/llvm/tools/llubi/lib/Value.h
index 41f7d789e44e2..aa2df04003d30 100644
--- a/llvm/tools/llubi/lib/Value.h
+++ b/llvm/tools/llubi/lib/Value.h
@@ -104,7 +104,7 @@ enum class StorageKind {
 /// Tri-state boolean value.
 enum class BooleanKind { False, True, Poison };
 
-/// A set of previously exposed provenance. It is originally yielded by
+/// A set of previously exposed provenances. It is originally yielded by
 /// inttoptr, and shared by pointers derived from the result.
 class WildcardProvenance : public RefCountedBase<WildcardProvenance> {
   // Each capability check may invalidate some provenances. If we cannot
@@ -114,24 +114,38 @@ class WildcardProvenance : public RefCountedBase<WildcardProvenance> {
   // the exposed provenances in the snapshot, which is captured at the
   // point where inttoptr executes.
   APInt ActiveMask;
-  uint64_t BaseAddress;
+  union {
+    // Initially, we record a snapshot of exposed provenances. It is active when
+    // ActiveMask is zero.
+    uint64_t Generation;
+    // After a memory access is performed, the memory object can be determined.
+    // It is active when ActiveMask is non-zero.
+    uint64_t BaseAddress;
+  };
 
   friend class Context;
 
 public:
-  WildcardProvenance(uint64_t BaseAddress, uint32_t Generation)
-      : ActiveMask(APInt::getAllOnes(Generation)), BaseAddress(BaseAddress) {}
+  explicit WildcardProvenance(uint64_t Generation)
+      : ActiveMask(), Generation(Generation) {}
 };
 
 /// Components of a pointer excluding address. They are shared between pointer
 /// values, as most of operations don't change the provenance.
 /// Each node will be assigned a unique, pointer-sized tag, which is used to
 /// represent the pointer in the memory.
+/// The provenance can be either concrete or wildcard, as determined by the
+/// cases below: Obj        Wildcard      State Null       Null          Invalid
+/// Null       NonNull       Wildcard
+/// NonNull    Null          Concrete
+/// NonNull    NonNull       Wildcard (limited to exposed provenances associated
+/// with a specific memory object)
 class Provenance : public RefCountedBase<Provenance> {
   // TODO: store reference to the provenance of the pointer it is derived from
 
   // The underlying memory object. It can be null for invalid or dangling
-  // pointers.
+  // pointers. Besides, for pointers with wildcard provenance, it can be null
+  // until the memory object is resolved by gep inbounds.
   IntrusiveRefCntPtr<MemoryObject> Obj;
 
   // A tag is a randomly generated unique identifier to recover the provenance
@@ -156,7 +170,9 @@ class Provenance : public RefCountedBase<Provenance> {
 public:
   Provenance(IntrusiveRefCntPtr<MemoryObject> Obj) : Obj(std::move(Obj)) {}
   static IntrusiveRefCntPtr<Provenance> nullary();
+  IntrusiveRefCntPtr<Provenance> getWithKnownMemoryObject(MemoryObject &Obj);
   MemoryObject *getMemoryObject() const { return Obj.get(); }
+  bool isWildcard() const { return Wildcard != nullptr; }
 };
 
 class Pointer {
@@ -176,12 +192,14 @@ class Pointer {
   Pointer getWithNewAddr(const APInt &NewAddr) const {
     return Pointer(Prov, NewAddr);
   }
+  Pointer getWithNewProvenance(IntrusiveRefCntPtr<Provenance> NewProv) const {
+    return Pointer(NewProv, Address);
+  }
   static AnyValue null(unsigned AS, const DataLayout &DL);
   bool isNullPtr(unsigned AS, const DataLayout &DL) const;
   void print(raw_ostream &OS) const;
   const APInt &address() const { return Address; }
   Provenance &provenance() const { return *Prov; }
-  MemoryObject *getMemoryObject() const { return Prov->getMemoryObject(); }
 };
 
 // Value representation for actual values of LLVM values.

>From 69fac30058554ae93d42450442974a685ae520f9 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Wed, 3 Jun 2026 02:35:09 +0800
Subject: [PATCH 4/7] [llubi] Fix compilation error after rebasing

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

diff --git a/llvm/tools/llubi/lib/Context.cpp b/llvm/tools/llubi/lib/Context.cpp
index 1eb7160dc07e1..c0939de6104db 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -71,7 +71,7 @@ bool Context::initGlobalValues() {
     if (!GV.hasInitializer())
       continue;
 
-    MemoryObject *Obj = GlobalAddrMap.at(&GV).getMemoryObject();
+    MemoryObject *Obj = GlobalAddrMap.at(&GV).provenance().getMemoryObject();
     assert(Obj && "global pointer should have memory object provenance");
 
     Constant *Init = GV.getInitializer();
diff --git a/llvm/tools/llubi/lib/ExecutorBase.cpp b/llvm/tools/llubi/lib/ExecutorBase.cpp
index 4c0afca7fa69c..aa3abdfb511e0 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.cpp
+++ b/llvm/tools/llubi/lib/ExecutorBase.cpp
@@ -147,8 +147,14 @@ void ExecutorBase::store(const AnyValue &Ptr, Align Alignment,
   if (auto [MO, Offset] = verifyMemAccess(
           PtrVal, Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
           /*IsStore=*/true, AS);
-      MO)
+      MO) {
+    if (MO->isConstant()) {
+      reportImmediateUB() << "Try to write to a constant memory object: "
+                          << PtrVal << ".";
+      return;
+    }
     Ctx.store(*MO, Offset, Val, ValTy);
+  }
 }
 
 void ExecutorBase::requestProgramExit(ProgramExitInfo::ProgramExitKind Kind,

>From c12b74446803a675e096fbc359725da831a1afbc Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Sat, 13 Jun 2026 22:47:42 +0800
Subject: [PATCH 5/7] [llubi] Address review comments.

---
 llvm/tools/llubi/lib/Context.cpp      | 42 +++++++++++++----------
 llvm/tools/llubi/lib/Context.h        | 13 ++++----
 llvm/tools/llubi/lib/ExecutorBase.cpp | 23 ++++++-------
 llvm/tools/llubi/lib/ExecutorBase.h   | 13 ++++----
 llvm/tools/llubi/lib/Interpreter.cpp  | 48 +++++++++++----------------
 llvm/tools/llubi/lib/Library.cpp      | 18 ++++------
 llvm/tools/llubi/lib/Value.h          | 37 ++++++++++++---------
 7 files changed, 96 insertions(+), 98 deletions(-)

diff --git a/llvm/tools/llubi/lib/Context.cpp b/llvm/tools/llubi/lib/Context.cpp
index c0939de6104db..6a579c3ef6ab4 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -632,7 +632,7 @@ void Context::exposeProvenance(Provenance &Prov) {
 MemoryObject *
 Context::checkProvenance(const Pointer &Ptr,
                          function_ref<bool(const Provenance &)> Check,
-                         unsigned AS, bool HasSideEffect) {
+                         bool HasSideEffect) {
   auto &Prov = Ptr.provenance();
   if (!Check(Prov))
     return nullptr;
@@ -641,10 +641,10 @@ Context::checkProvenance(const Pointer &Ptr,
     return Prov.Obj.get();
 
   MemoryObject *MO = nullptr;
-  APInt TempMask;
-  APInt *Mask = HasSideEffect ? &Prov.Wildcard->ActiveMask : &TempMask;
+  APInt &Mask = Prov.Wildcard->ActiveMask;
   SmallVector<IntrusiveRefCntPtr<Provenance>> *List = nullptr;
-  if (Prov.Wildcard->ActiveMask.isZero()) {
+  uint32_t ProvenanceCount = 0;
+  if (Mask.isZero()) {
     // The memory object hasn't been determined.
     uint64_t Addr = Ptr.address().getLimitedValue();
     auto Iter = ExposedProvenances.upper_bound(Addr);
@@ -652,27 +652,31 @@ Context::checkProvenance(const Pointer &Ptr,
       return nullptr;
     auto &[BaseAddress, Set] = *std::prev(Iter);
     auto &Obj = MemoryObjects.at(BaseAddress);
-    if (Obj->getAddressSpace() != AS || !Obj->inBounds(Ptr.address()))
+    if (!Obj->inBounds(Ptr.address()))
       return nullptr;
     MO = Obj.get();
-    uint32_t Generation = std::distance(
+    // We only inspect the first N exposed provenances according to the global
+    // generation number of the wildcard pointer.
+    ProvenanceCount = std::distance(
         Set.GenerationList.begin(),
         upper_bound(Set.GenerationList, Prov.Wildcard->Generation));
-    *Mask = APInt::getAllOnes(Generation);
+    if (HasSideEffect) {
+      Mask = APInt::getAllOnes(ProvenanceCount);
+      Prov.Wildcard->BaseAddress = BaseAddress;
+    }
     List = &Set.List;
   } else {
-    // We already determined the memory object in a previous memory access。
+    // We already determined the memory object in a previous memory access.
     uint64_t BaseAddress = Prov.Wildcard->BaseAddress;
     auto Iter = ExposedProvenances.find(BaseAddress);
     // The memory object has been freed.
     if (Iter == ExposedProvenances.end())
       return nullptr;
     MO = MemoryObjects.at(BaseAddress).get();
-    if (MO->getAddressSpace() != AS || !MO->inBounds(Ptr.address()))
+    if (!MO->inBounds(Ptr.address()))
       return nullptr;
-    if (HasSideEffect)
-      TempMask = Prov.Wildcard->ActiveMask;
     List = &Iter->second.List;
+    ProvenanceCount = Mask.getBitWidth();
   }
   if (Prov.Obj) {
     // We already determined the memory object via speculatable operations like
@@ -681,15 +685,19 @@ Context::checkProvenance(const Pointer &Ptr,
       return nullptr;
   }
 
-  uint32_t Generation = Mask->getBitWidth();
   bool Valid = false;
-  for (uint32_t I = 0; I != Generation; ++I) {
-    if (!(*Mask)[I])
+  for (uint32_t I = 0; I != ProvenanceCount; ++I) {
+    assert((!HasSideEffect || !Mask.isZero()) &&
+           "Mask must be initialized if HasSideEffect is true.");
+    if (!Mask.isZero() && !Mask[I])
       continue;
-    if (Check(*(*List)[I]))
+    if (Check(*(*List)[I])) {
       Valid = true;
-    else
-      Mask->clearBit(I);
+      // Early return as we don't need to update the Mask.
+      if (!HasSideEffect)
+        break;
+    } else if (HasSideEffect)
+      Mask.clearBit(I);
   }
 
   return Valid ? MO : nullptr;
diff --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index e805f734acf08..9be7fb84aa7df 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -245,12 +245,11 @@ class Context {
   //   1. Each address maps to at most one memory object during the execution of
   //   the program, as AllocationBase increases monotonically.
   //   2. We maintain the set of exposed provenances. When ptrtoint/addr
-  //   executes,
-  //      the provenance is inserted to the set. When inttoptr executes, it
-  //      yields a pointer with a wildcard provenance. That is, each later use
-  //      will check whether there is an exposed provenance in the snapshot
-  //      allowing the operation. The invalid provenance will be masked out
-  //      after the operation. If we cannot pick one, it is UB.
+  //   executes, the provenance is inserted to the set. When inttoptr executes,
+  //   it yields a pointer with a wildcard provenance. That is, each later use
+  //   will check whether there is an exposed provenance in the snapshot
+  //   allowing the operation. The invalid provenance will be masked out after
+  //   the operation. If we cannot pick one, it is UB.
 
   /// Exposed provenances are grouped by associated memory objects for efficient
   /// invalidation.
@@ -367,7 +366,7 @@ class Context {
   /// must report UB when the result is nullptr.
   MemoryObject *checkProvenance(const Pointer &Ptr,
                                 function_ref<bool(const Provenance &)> Check,
-                                unsigned AS, bool HasSideEffect = true);
+                                bool HasSideEffect = true);
   /// Returns the snapshot of currently exposed provenances.
   IntrusiveRefCntPtr<Provenance> getWildcardProvenance();
   /// Convert byte sequence to a value of the given type. Uninitialized bits are
diff --git a/llvm/tools/llubi/lib/ExecutorBase.cpp b/llvm/tools/llubi/lib/ExecutorBase.cpp
index aa3abdfb511e0..482930af81b5b 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.cpp
+++ b/llvm/tools/llubi/lib/ExecutorBase.cpp
@@ -55,15 +55,12 @@ void ExecutorBase::reportErrorString(StringRef Msg) {
 
 std::pair<MemoryObject *, uint64_t>
 ExecutorBase::verifyMemAccess(const Pointer &Ptr, uint64_t AccessSize,
-                              Align Alignment, bool IsStore, unsigned AS) {
-  auto *MO = Ctx.checkProvenance(
-      Ptr,
-      [](const Provenance &) {
-        // TODO: check provenance
-        // TODO: check inrange(S, E)
-        return true;
-      },
-      AS);
+                              Align Alignment, bool IsStore) {
+  auto *MO = Ctx.checkProvenance(Ptr, [](const Provenance &) {
+    // TODO: check provenance
+    // TODO: check inrange(S, E)
+    return true;
+  });
   if (!MO) {
     reportImmediateUB()
         << "Invalid memory access via a pointer with nullary provenance.";
@@ -113,7 +110,7 @@ ExecutorBase::verifyMemAccess(const Pointer &Ptr, uint64_t AccessSize,
 }
 
 AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
-                            bool NoUndef, unsigned AS) {
+                            bool NoUndef) {
   if (Ptr.isPoison()) {
     reportImmediateUB() << "Invalid memory access with a poison pointer.";
     return AnyValue::getPoisonValue(Ctx, ValTy);
@@ -121,7 +118,7 @@ AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
   auto &PtrVal = Ptr.asPointer();
   if (auto [MO, Offset] = verifyMemAccess(
           PtrVal, Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
-          /*IsStore=*/false, AS);
+          /*IsStore=*/false);
       MO) {
     // Load from a dead stack object yields poison value.
     if (MO->getState() == MemoryObjectState::Dead)
@@ -138,7 +135,7 @@ AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
 }
 
 void ExecutorBase::store(const AnyValue &Ptr, Align Alignment,
-                         const AnyValue &Val, Type *ValTy, unsigned AS) {
+                         const AnyValue &Val, Type *ValTy) {
   if (Ptr.isPoison()) {
     reportImmediateUB() << "Invalid memory access with a poison pointer.";
     return;
@@ -146,7 +143,7 @@ void ExecutorBase::store(const AnyValue &Ptr, Align Alignment,
   auto &PtrVal = Ptr.asPointer();
   if (auto [MO, Offset] = verifyMemAccess(
           PtrVal, Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
-          /*IsStore=*/true, AS);
+          /*IsStore=*/true);
       MO) {
     if (MO->isConstant()) {
       reportImmediateUB() << "Try to write to a constant memory object: "
diff --git a/llvm/tools/llubi/lib/ExecutorBase.h b/llvm/tools/llubi/lib/ExecutorBase.h
index 35a37f2b3c80d..d165192b55d63 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.h
+++ b/llvm/tools/llubi/lib/ExecutorBase.h
@@ -101,14 +101,15 @@ class ExecutorBase {
 
   /// Check if the upcoming memory access is valid. Returns the resolved memory
   /// object and offset if it is valid.
-  std::pair<MemoryObject *, uint64_t>
-  verifyMemAccess(const Pointer &Ptr, uint64_t AccessSize, Align Alignment,
-                  bool IsStore, unsigned AS);
+  std::pair<MemoryObject *, uint64_t> verifyMemAccess(const Pointer &Ptr,
+                                                      uint64_t AccessSize,
+                                                      Align Alignment,
+                                                      bool IsStore);
 
-  AnyValue load(const AnyValue &Ptr, Align Alignment, Type *ValTy, bool NoUndef,
-                unsigned AS);
+  AnyValue load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
+                bool NoUndef);
   void store(const AnyValue &Ptr, Align Alignment, const AnyValue &Val,
-             Type *ValTy, unsigned AS);
+             Type *ValTy);
 
   void requestProgramExit(ProgramExitInfo::ProgramExitKind Kind,
                           uint64_t ExitCode = 0);
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 0e66a25564a1f..b5a7b0d26475c 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -131,14 +131,11 @@ static bool violatesDereferenceableBytesAttr(const AnyValue &V, uint64_t Bytes,
     return true;
   }
 
-  auto *MO = Ctx.checkProvenance(
-      Ptr,
-      [&](const Provenance &) {
-        // TODO: check read_provenance
-        // TODO: check nofree for attributes/metadata.
-        return true;
-      },
-      AS);
+  auto *MO = Ctx.checkProvenance(Ptr, [&](const Provenance &) {
+    // TODO: check read_provenance
+    // TODO: check nofree for attributes/metadata.
+    return true;
+  });
   if (!MO)
     return true;
 
@@ -715,8 +712,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   AnyValue computePtrAdd(const Pointer &Ptr, const APInt &Offset,
-                         GEPNoWrapFlags Flags, AnyValue &AccumulatedOffset,
-                         unsigned AS) {
+                         GEPNoWrapFlags Flags, AnyValue &AccumulatedOffset) {
     if (Offset.isZero())
       return Ptr;
     APInt IndexBits = Ptr.address().trunc(Offset.getBitWidth());
@@ -739,7 +735,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     MemoryObject *MO = nullptr;
     if (Flags.isInBounds()) {
       MO = Ctx.checkProvenance(
-          Ptr, [](const Provenance &) { return true; }, AS,
+          Ptr, [](const Provenance &) { return true; },
           /*HasSideEffect=*/false);
       if (!MO || !MO->inBounds(NewAddr))
         return AnyValue::poison();
@@ -765,29 +761,27 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   AnyValue computePtrAdd(const AnyValue &Ptr, const APInt &Offset,
-                         GEPNoWrapFlags Flags, AnyValue &AccumulatedOffset,
-                         unsigned AS) {
+                         GEPNoWrapFlags Flags, AnyValue &AccumulatedOffset) {
     if (Ptr.isPoison())
       return AnyValue::poison();
-    return computePtrAdd(Ptr.asPointer(), Offset, Flags, AccumulatedOffset, AS);
+    return computePtrAdd(Ptr.asPointer(), Offset, Flags, AccumulatedOffset);
   }
 
   AnyValue computeScaledPtrAdd(const AnyValue &Ptr, const AnyValue &Index,
                                const APInt &Scale, GEPNoWrapFlags Flags,
-                               AnyValue &AccumulatedOffset, unsigned AS) {
+                               AnyValue &AccumulatedOffset) {
     if (Ptr.isPoison() || Index.isPoison())
       return AnyValue::poison();
     assert(Ptr.isPointer() && Index.isInteger() && "Unexpected type.");
     if (Scale.isOne())
-      return computePtrAdd(Ptr, Index.asInteger(), Flags, AccumulatedOffset,
-                           AS);
+      return computePtrAdd(Ptr, Index.asInteger(), Flags, AccumulatedOffset);
     auto ScaledOffset =
         mulNoWrap(Index.asInteger(), Scale, Flags.hasNoUnsignedSignedWrap(),
                   Flags.hasNoUnsignedWrap());
     if (ScaledOffset.isPoison())
       return AnyValue::poison();
     return computePtrAdd(Ptr, ScaledOffset.asInteger(), Flags,
-                         AccumulatedOffset, AS);
+                         AccumulatedOffset);
   }
 
   AnyValue canonicalizeIndex(const AnyValue &Idx, unsigned IndexBitWidth,
@@ -1027,9 +1021,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
       auto Ptr = Args[0];
       if (Ptr.isPoison())
         return AnyValue();
-      auto *MO = Ctx.checkProvenance(
-          Ptr.asPointer(), [](const Provenance &) { return true; },
-          CB.getOperand(0)->getType()->getPointerAddressSpace());
+      auto *MO = Ctx.checkProvenance(Ptr.asPointer(),
+                                     [](const Provenance &) { return true; });
       assert(MO && "Memory object accessed by lifetime intrinsic should be "
                    "always valid.");
       if (IID == Intrinsic::lifetime_start) {
@@ -2368,7 +2361,6 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   void visitGetElementPtrInst(GetElementPtrInst &GEP) {
     uint32_t IndexBitWidth =
         DL.getIndexSizeInBits(GEP.getType()->getPointerAddressSpace());
-    unsigned AS = GEP.getType()->getPointerAddressSpace();
     GEPNoWrapFlags Flags = GEP.getNoWrapFlags();
     AnyValue Res = getValue(GEP.getPointerOperand());
     AnyValue AccumulatedOffset = APInt(IndexBitWidth, 0);
@@ -2387,7 +2379,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
                  AccumulatedOffset.asAggregate()))
           ResElem = computeScaledPtrAdd(
               ResElem, canonicalizeIndex(IndexElem, IndexBitWidth, Flags),
-              Scale, Flags, OffsetElem, AS);
+              Scale, Flags, OffsetElem);
       } else {
         AnyValue CanonicalIndex =
             canonicalizeIndex(Index, IndexBitWidth, Flags);
@@ -2395,10 +2387,10 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
           for (auto &&[ResElem, OffsetElem] :
                zip(Res.asAggregate(), AccumulatedOffset.asAggregate()))
             ResElem = computeScaledPtrAdd(ResElem, CanonicalIndex, Scale, Flags,
-                                          OffsetElem, AS);
+                                          OffsetElem);
         } else {
           Res = computeScaledPtrAdd(Res, CanonicalIndex, Scale, Flags,
-                                    AccumulatedOffset, AS);
+                                    AccumulatedOffset);
         }
       }
     };
@@ -2465,8 +2457,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
 
   void visitLoadInst(LoadInst &LI) {
     auto RetVal = load(getValue(LI.getPointerOperand()), LI.getAlign(),
-                       LI.getType(), LI.hasMetadata(LLVMContext::MD_noundef),
-                       LI.getPointerAddressSpace());
+                       LI.getType(), LI.hasMetadata(LLVMContext::MD_noundef));
     // TODO: track volatile loads
     handleMetadata(LI.getType(), RetVal, LI);
     setResult(LI, std::move(RetVal));
@@ -2477,8 +2468,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     auto &Val = getValue(SI.getValueOperand());
     // TODO: track volatile stores
     // TODO: handle metadata
-    store(Ptr, SI.getAlign(), Val, SI.getValueOperand()->getType(),
-          SI.getPointerAddressSpace());
+    store(Ptr, SI.getAlign(), Val, SI.getValueOperand()->getType());
     if (!hasProgramExited() && !Handler.onInstructionExecuted(SI, AnyValue()))
       setFailed();
   }
diff --git a/llvm/tools/llubi/lib/Library.cpp b/llvm/tools/llubi/lib/Library.cpp
index 4bdafe8db96ab..c3452b8af92c0 100644
--- a/llvm/tools/llubi/lib/Library.cpp
+++ b/llvm/tools/llubi/lib/Library.cpp
@@ -34,9 +34,8 @@ std::optional<std::string> Library::readStringFromMemory(const Pointer &Ptr) {
   uint64_t Offset = 0;
 
   while (true) {
-    auto [MO, ValidOffset] =
-        Executor.verifyMemAccess(Ptr.getWithNewAddr(Address + Offset), 1,
-                                 Align(1), /*IsStore=*/false, /*AS=*/0);
+    auto [MO, ValidOffset] = Executor.verifyMemAccess(
+        Ptr.getWithNewAddr(Address + Offset), 1, Align(1), /*IsStore=*/false);
     if (!MO)
       return std::nullopt;
 
@@ -122,13 +121,10 @@ AnyValue Library::executeFree(ArrayRef<AnyValue> Args) {
   if (Ptr.isNullPtr(/*AS=*/0, DL))
     return AnyValue();
 
-  MemoryObject *Obj = Ctx.checkProvenance(
-      Ptr,
-      [](const Provenance &) {
-        // TODO: check nofree
-        return true;
-      },
-      /*AS=*/0);
+  MemoryObject *Obj = Ctx.checkProvenance(Ptr, [](const Provenance &) {
+    // TODO: check nofree
+    return true;
+  });
   if (!Obj) {
     Executor.reportImmediateUB()
         << "freeing a pointer with nullary provenance.";
@@ -282,7 +278,7 @@ AnyValue Library::executePrintf(ArrayRef<AnyValue> Args) {
     case 'n': {
       OS.flush();
       Executor.store(Arg, Align(4), AnyValue(APInt(32, Output.size())),
-                     Type::getInt32Ty(Ctx.getContext()), /*AS=*/0);
+                     Type::getInt32Ty(Ctx.getContext()));
       break;
     }
     case 'p': {
diff --git a/llvm/tools/llubi/lib/Value.h b/llvm/tools/llubi/lib/Value.h
index aa2df04003d30..c8242af511498 100644
--- a/llvm/tools/llubi/lib/Value.h
+++ b/llvm/tools/llubi/lib/Value.h
@@ -106,20 +106,26 @@ enum class BooleanKind { False, True, Poison };
 
 /// A set of previously exposed provenances. It is originally yielded by
 /// inttoptr, and shared by pointers derived from the result.
+///
+/// Each capability check may invalidate some provenances. If we cannot
+/// pick one, it is UB. That is, from the angelic non-determinism view,
+/// we cannot pick a provenance to make the program reach this point.
+///
+/// For efficiency, this class has different forms in two stages:
+/// 1. Before any memory access is performed, ActiveMask is set to zero and
+/// Generation represents the global generation number of the snapshot.
+/// 2. After a memory access is performed, we can determine exactly one memory
+/// object to be accessed (address ranges are distinct). In this case,
+/// BaseAddress is set and ActiveMask is non-zero. ActiveMask represents the
+/// validity of first N exposed provenances associated with the memory object.
+/// The bitwidth N is the first index I in the provenance list with
+/// List[I].Generation > WildcardProvenance::Generation. That is, we can only
+/// access through exposed provenances before inttoptr executes.
+/// Note that if ActiveMask becomes zero again, UB must be triggered.
 class WildcardProvenance : public RefCountedBase<WildcardProvenance> {
-  // Each capability check may invalidate some provenances. If we cannot
-  // pick one, it is UB. That is, from the angelic non-determinism view,
-  // we cannot pick a provenance to make the program reach this point.
-  // The bitwidth represents the generation of the set. We only consider
-  // the exposed provenances in the snapshot, which is captured at the
-  // point where inttoptr executes.
   APInt ActiveMask;
   union {
-    // Initially, we record a snapshot of exposed provenances. It is active when
-    // ActiveMask is zero.
     uint64_t Generation;
-    // After a memory access is performed, the memory object can be determined.
-    // It is active when ActiveMask is non-zero.
     uint64_t BaseAddress;
   };
 
@@ -135,11 +141,12 @@ class WildcardProvenance : public RefCountedBase<WildcardProvenance> {
 /// Each node will be assigned a unique, pointer-sized tag, which is used to
 /// represent the pointer in the memory.
 /// The provenance can be either concrete or wildcard, as determined by the
-/// cases below: Obj        Wildcard      State Null       Null          Invalid
-/// Null       NonNull       Wildcard
-/// NonNull    Null          Concrete
-/// NonNull    NonNull       Wildcard (limited to exposed provenances associated
-/// with a specific memory object)
+/// cases below:
+///  Obj        Wildcard      State
+///  Null       Null          Invalid
+///  Null       NonNull       Wildcard
+///  NonNull    Null          Concrete
+///  NonNull    NonNull       Wildcard (associated with a specific MO)
 class Provenance : public RefCountedBase<Provenance> {
   // TODO: store reference to the provenance of the pointer it is derived from
 

>From 6073cca3046b12b530372dca46800d7cab62ca37 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Sat, 13 Jun 2026 22:54:03 +0800
Subject: [PATCH 6/7] [llubi] Merge lists. NFC.

---
 llvm/tools/llubi/lib/Context.cpp | 15 +++++++--------
 llvm/tools/llubi/lib/Context.h   | 12 ++++++++++--
 2 files changed, 17 insertions(+), 10 deletions(-)

diff --git a/llvm/tools/llubi/lib/Context.cpp b/llvm/tools/llubi/lib/Context.cpp
index 6a579c3ef6ab4..7b9d0d849e5f3 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -623,10 +623,8 @@ void Context::exposeProvenance(Provenance &Prov) {
     return;
   uint64_t Address = Obj->getAddress();
   ExposedProvenanceSet &Set = ExposedProvenances[Address];
-  if (Set.Set.insert(&Prov).second) {
-    Set.List.emplace_back(&Prov);
-    Set.GenerationList.push_back(++ExposedProvenanceSetGeneration);
-  }
+  if (Set.Set.insert(&Prov).second)
+    Set.List.push_back({&Prov, ++ExposedProvenanceSetGeneration});
 }
 
 MemoryObject *
@@ -642,7 +640,7 @@ Context::checkProvenance(const Pointer &Ptr,
 
   MemoryObject *MO = nullptr;
   APInt &Mask = Prov.Wildcard->ActiveMask;
-  SmallVector<IntrusiveRefCntPtr<Provenance>> *List = nullptr;
+  SmallVector<ExposedProvenance> *List = nullptr;
   uint32_t ProvenanceCount = 0;
   if (Mask.isZero()) {
     // The memory object hasn't been determined.
@@ -658,8 +656,9 @@ Context::checkProvenance(const Pointer &Ptr,
     // We only inspect the first N exposed provenances according to the global
     // generation number of the wildcard pointer.
     ProvenanceCount = std::distance(
-        Set.GenerationList.begin(),
-        upper_bound(Set.GenerationList, Prov.Wildcard->Generation));
+        Set.List.begin(),
+        upper_bound(Set.List,
+                    ExposedProvenance{nullptr, Prov.Wildcard->Generation}));
     if (HasSideEffect) {
       Mask = APInt::getAllOnes(ProvenanceCount);
       Prov.Wildcard->BaseAddress = BaseAddress;
@@ -691,7 +690,7 @@ Context::checkProvenance(const Pointer &Ptr,
            "Mask must be initialized if HasSideEffect is true.");
     if (!Mask.isZero() && !Mask[I])
       continue;
-    if (Check(*(*List)[I])) {
+    if (Check(*(*List)[I].Prov)) {
       Valid = true;
       // Early return as we don't need to update the Mask.
       if (!HasSideEffect)
diff --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index 9be7fb84aa7df..f15d1c8d8ac81 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -253,9 +253,17 @@ class Context {
 
   /// Exposed provenances are grouped by associated memory objects for efficient
   /// invalidation.
+  struct ExposedProvenance {
+    IntrusiveRefCntPtr<Provenance> Prov;
+    uint64_t Generation;
+
+    bool operator<(const ExposedProvenance &RHS) const {
+      return Generation < RHS.Generation;
+    }
+  };
   struct ExposedProvenanceSet {
-    SmallVector<IntrusiveRefCntPtr<Provenance>> List;
-    SmallVector<uint64_t> GenerationList;
+    // (Provenance, Generation)
+    SmallVector<ExposedProvenance> List;
     // FIXME: Implement a partial order comparator for provenance instead of
     // deduplicating by pointers.
     SmallPtrSet<Provenance *, 4> Set;

>From f1af251abfa5f5f87f77032278f84565a46c84c5 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Sat, 13 Jun 2026 23:16:56 +0800
Subject: [PATCH 7/7] [llubi] Add more negative tests. NFC.

---
 llvm/test/tools/llubi/inttoptr_generation.ll | 34 +++++++++++++++++
 llvm/test/tools/llubi/inttoptr_multiobj.ll   | 40 ++++++++++++++++++++
 2 files changed, 74 insertions(+)
 create mode 100644 llvm/test/tools/llubi/inttoptr_generation.ll
 create mode 100644 llvm/test/tools/llubi/inttoptr_multiobj.ll

diff --git a/llvm/test/tools/llubi/inttoptr_generation.ll b/llvm/test/tools/llubi/inttoptr_generation.ll
new file mode 100644
index 0000000000000..575a669f785fd
--- /dev/null
+++ b/llvm/test/tools/llubi/inttoptr_generation.ll
@@ -0,0 +1,34 @@
+; 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
+
+target datalayout = "e-p:64:64:64"
+
+define void @main() {
+  ; Assume the pointer address is 8.
+  %alloc = alloca i32
+  %ptr1 = inttoptr i64 8 to ptr
+  %int = ptrtoint ptr %alloc to i64
+  %ptr2 = inttoptr i64 %int to ptr
+  %ptr3 = inttoptr i64 8 to ptr
+
+  ; We can access the wildcard pointer through exposed provenances.
+  store i32 0, ptr %ptr2
+  ; We can access the wildcard pointer through exposed provenances, even if there are no dependencies.
+  store i32 0, ptr %ptr3
+  ; We cannot access the pointer created before any provenance is exposed.
+  store i32 0, ptr %ptr1
+
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x8 [alloc]
+; CHECK-NEXT:   %ptr1 = inttoptr i64 8 to ptr => ptr 0x8 [nullary]
+; CHECK-NEXT:   %int = ptrtoint ptr %alloc to i64 => i64 8
+; CHECK-NEXT:   %ptr2 = inttoptr i64 %int to ptr => ptr 0x8 [wildcard]
+; CHECK-NEXT:   %ptr3 = inttoptr i64 8 to ptr => ptr 0x8 [wildcard]
+; CHECK-NEXT:   store i32 0, ptr %ptr2, align 4
+; CHECK-NEXT:   store i32 0, ptr %ptr3, align 4
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i32 0, ptr %ptr1, align 4 at @main <stdin>:19
+; 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/inttoptr_multiobj.ll b/llvm/test/tools/llubi/inttoptr_multiobj.ll
new file mode 100644
index 0000000000000..78dde07cc64b0
--- /dev/null
+++ b/llvm/test/tools/llubi/inttoptr_multiobj.ll
@@ -0,0 +1,40 @@
+; 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
+
+target datalayout = "e-p:64:64:64"
+
+define void @main() {
+  %alloc1 = alloca i32
+  %alloc2 = alloca i32
+
+  %addr1 = ptrtoint ptr %alloc1 to i64
+  %addr2 = ptrtoint ptr %alloc2 to i64
+  %ptrdiff = sub i64 %addr2, %addr1
+
+  %p = inttoptr i64 %addr1 to ptr
+
+  %gep1 = getelementptr i8, ptr %p, i64 %ptrdiff
+  %gep2 = getelementptr inbounds i8, ptr %gep1, i64 2
+
+  ; We can access %alloc1, although %gep2 has been resolved to %alloc2
+  store i32 0, ptr %p
+
+  ; Now we cannot access %alloc2 through %gep2, since the wildcard provenance has been resolved to %alloc1.
+  store i16 0, ptr %gep2
+
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %alloc1 = alloca i32, align 4 => ptr 0x8 [alloc1]
+; CHECK-NEXT:   %alloc2 = alloca i32, align 4 => ptr 0x10 [alloc2]
+; CHECK-NEXT:   %addr1 = ptrtoint ptr %alloc1 to i64 => i64 8
+; CHECK-NEXT:   %addr2 = ptrtoint ptr %alloc2 to i64 => i64 16
+; CHECK-NEXT:   %ptrdiff = sub i64 %addr2, %addr1 => i64 8
+; CHECK-NEXT:   %p = inttoptr i64 %addr1 to ptr => ptr 0x8 [wildcard]
+; CHECK-NEXT:   %gep1 = getelementptr i8, ptr %p, i64 %ptrdiff => ptr 0x10 [wildcard]
+; CHECK-NEXT:   %gep2 = getelementptr inbounds i8, ptr %gep1, i64 2 => ptr 0x12 [alloc2 + 2]
+; CHECK-NEXT:   store i32 0, ptr %p, align 4
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i16 0, ptr %gep2, align 2 at @main <stdin>:23
+; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
+; CHECK-NEXT: error: Execution of function 'main' failed.



More information about the llvm-commits mailing list