[llvm] [llubi] Experimental support for noalias (PR #195808)

Zhige Chen via llvm-commits llvm-commits at lists.llvm.org
Tue May 5 01:54:17 PDT 2026


https://github.com/nofe1248 created https://github.com/llvm/llvm-project/pull/195808

This PR introduces initial, experimental support for `noalias` checking, building upon #185977. For more details regarding the model, please refer to the blog https://jhostert.de/blog/2025/noalias/ and the original Tree Borrows paper https://dl.acm.org/doi/pdf/10.1145/3735592.

Currently, the implementation:
- Tracks protected byte ranges using sparse/RLE-style interval state.
- Checks loads and stores against active protectors.
- Handles protector end actions.
- Prunes inactive `noalias` nodes when they have no active descendants in the tree.

Note: This PR does not yet account for interactions with other LLVM IR components, such as the `dereferenceable` attribute.

>From 9e98898c1e54788dddbec368661e87a6d3c242a1 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Thu, 12 Mar 2026 04:00:05 +0800
Subject: [PATCH 1/6] [llubi] Add basic support for provenance modeling

---
 llvm/test/tools/llubi/alloca.ll        |   2 +-
 llvm/test/tools/llubi/bitcast_be.ll    |   3 +-
 llvm/test/tools/llubi/bitcast_le.ll    |   3 +-
 llvm/test/tools/llubi/freeze.ll        |   6 +-
 llvm/test/tools/llubi/gep.ll           |  28 +++----
 llvm/test/tools/llubi/inttoptr.ll      |   6 +-
 llvm/test/tools/llubi/loadstore_be.ll  |  18 ++++-
 llvm/test/tools/llubi/loadstore_le.ll  |  17 +++-
 llvm/test/tools/llubi/loadstore_uaf.ll |   2 +-
 llvm/test/tools/llubi/store_dead.ll    |   2 +-
 llvm/tools/llubi/lib/Context.cpp       | 107 ++++++++++++++++++++-----
 llvm/tools/llubi/lib/Context.h         |  33 ++++++--
 llvm/tools/llubi/lib/Value.cpp         |   8 +-
 llvm/tools/llubi/lib/Value.h           |  35 +++++---
 14 files changed, 199 insertions(+), 71 deletions(-)

diff --git a/llvm/test/tools/llubi/alloca.ll b/llvm/test/tools/llubi/alloca.ll
index 51f50dff67fc0..e45d499d3205b 100644
--- a/llvm/test/tools/llubi/alloca.ll
+++ b/llvm/test/tools/llubi/alloca.ll
@@ -36,6 +36,6 @@ define void @main() {
 ; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x70 [alloc]
 ; CHECK-NEXT:   ret ptr %alloc
 ; CHECK-NEXT: Exiting function: stack_address
-; CHECK-NEXT:   %ptr = call ptr @stack_address() => ptr 0x70 [dangling]
+; CHECK-NEXT:   %ptr = call ptr @stack_address() => ptr 0x70 [alloc (dangling)]
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/bitcast_be.ll b/llvm/test/tools/llubi/bitcast_be.ll
index 8e86f3f325251..5bd2f73df5617 100644
--- a/llvm/test/tools/llubi/bitcast_be.ll
+++ b/llvm/test/tools/llubi/bitcast_be.ll
@@ -13,7 +13,6 @@ entry:
   %bitcast_float2int = bitcast float 2.0 to i32
   %bitcast_half2bf16 = bitcast half 1.0 to bfloat
   %ptr = alloca i32
-  ; FIXME: The provenance is lost.
   %bitcast_ptr2ptr = bitcast ptr %ptr to ptr
 
   %bitcast_vec2scalar1 = bitcast <2 x i32> <i32 0, i32 1> to i64
@@ -42,7 +41,7 @@ entry:
 ; CHECK-NEXT:   %bitcast_float2int = bitcast float 2.000000e+00 to i32 => i32 1073741824
 ; CHECK-NEXT:   %bitcast_half2bf16 = bitcast half 0xH3C00 to bfloat => bfloat 7.812500e-03
 ; CHECK-NEXT:   %ptr = alloca i32, align 4 => ptr 0x8 [ptr]
-; CHECK-NEXT:   %bitcast_ptr2ptr = bitcast ptr %ptr to ptr => ptr 0x8 [dangling]
+; CHECK-NEXT:   %bitcast_ptr2ptr = bitcast ptr %ptr to ptr => ptr 0x8 [ptr]
 ; CHECK-NEXT:   %bitcast_vec2scalar1 = bitcast <2 x i32> <i32 0, i32 1> to i64 => i64 1
 ; CHECK-NEXT:   %bitcast_vec2scalar2 = bitcast <4 x i4> <i4 1, i4 2, i4 3, i4 5> to i16 => i16 4661
 ; CHECK-NEXT:   %bitcast_scalar2vec1 = bitcast i64 1 to <2 x i32> => { i32 0, i32 1 }
diff --git a/llvm/test/tools/llubi/bitcast_le.ll b/llvm/test/tools/llubi/bitcast_le.ll
index 773e10feaac99..a0c4b84f7d804 100644
--- a/llvm/test/tools/llubi/bitcast_le.ll
+++ b/llvm/test/tools/llubi/bitcast_le.ll
@@ -13,7 +13,6 @@ entry:
   %bitcast_float2int = bitcast float 2.0 to i32
   %bitcast_half2bf16 = bitcast half 1.0 to bfloat
   %ptr = alloca i32
-  ; FIXME: The provenance is lost.
   %bitcast_ptr2ptr = bitcast ptr %ptr to ptr
 
   %bitcast_vec2scalar1 = bitcast <2 x i32> <i32 0, i32 1> to i64
@@ -42,7 +41,7 @@ entry:
 ; CHECK-NEXT:   %bitcast_float2int = bitcast float 2.000000e+00 to i32 => i32 1073741824
 ; CHECK-NEXT:   %bitcast_half2bf16 = bitcast half 0xH3C00 to bfloat => bfloat 7.812500e-03
 ; CHECK-NEXT:   %ptr = alloca i32, align 4 => ptr 0x8 [ptr]
-; CHECK-NEXT:   %bitcast_ptr2ptr = bitcast ptr %ptr to ptr => ptr 0x8 [dangling]
+; CHECK-NEXT:   %bitcast_ptr2ptr = bitcast ptr %ptr to ptr => ptr 0x8 [ptr]
 ; CHECK-NEXT:   %bitcast_vec2scalar1 = bitcast <2 x i32> <i32 0, i32 1> to i64 => i64 4294967296
 ; CHECK-NEXT:   %bitcast_vec2scalar2 = bitcast <4 x i4> <i4 1, i4 2, i4 3, i4 5> to i16 => i16 21281
 ; CHECK-NEXT:   %bitcast_scalar2vec1 = bitcast i64 1 to <2 x i32> => { i32 1, i32 0 }
diff --git a/llvm/test/tools/llubi/freeze.ll b/llvm/test/tools/llubi/freeze.ll
index cc79a8d897f4c..c63db99b08084 100644
--- a/llvm/test/tools/llubi/freeze.ll
+++ b/llvm/test/tools/llubi/freeze.ll
@@ -24,11 +24,11 @@ define void @main() {
 ; CHECK-NEXT:   %int_freeze_poison2 = freeze i32 poison => i32 1044445579
 ; CHECK-NEXT:   %float_freeze = freeze float 1.000000e+00 => float 1.000000e+00
 ; CHECK-NEXT:   %float_freeze_poison = freeze float poison => float 0xF032E8B9
-; CHECK-NEXT:   %ptr_freeze = freeze ptr null => ptr 0x0 [dangling]
-; CHECK-NEXT:   %ptr_freeze_poison = freeze ptr poison => ptr 0x98F56903CEE3FCEE [dangling]
+; CHECK-NEXT:   %ptr_freeze = freeze ptr null => ptr 0x0 [nullary]
+; CHECK-NEXT:   %ptr_freeze_poison = freeze ptr poison => ptr 0x98F56903CEE3FCEE [nullary]
 ; CHECK-NEXT:   %vec_freeze = freeze <2 x i32> <i32 10, i32 poison> => { i32 10, i32 990324140 }
 ; CHECK-NEXT:   %arr_freeze = freeze [2 x i32] [i32 10, i32 poison] => { i32 10, i32 -44045842 }
 ; CHECK-NEXT:   %struct_freeze = freeze { i32, float } { i32 10, float poison } => { i32 10, float 0x042AE12F }
-; CHECK-NEXT:   %struct_freeze_nested = freeze { i32, { float, ptr } } { i32 poison, { float, ptr } { float 1.000000e+01, ptr poison } } => { i32 1166165736, { float 1.000000e+01, ptr 0xD79E62976F604366 [dangling] } }
+; CHECK-NEXT:   %struct_freeze_nested = freeze { i32, { float, ptr } } { i32 poison, { float, ptr } { float 1.000000e+01, ptr poison } } => { i32 1166165736, { float 1.000000e+01, ptr 0xD79E62976F604366 [nullary] } }
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/gep.ll b/llvm/test/tools/llubi/gep.ll
index 4376569fa1811..3426249fa138c 100644
--- a/llvm/test/tools/llubi/gep.ll
+++ b/llvm/test/tools/llubi/gep.ll
@@ -100,16 +100,16 @@ define void @main() {
 ; CHECK-NEXT:   %gep_poison_idx = getelementptr i8, ptr %alloc, i64 poison => poison
 ; CHECK-NEXT:   %gep_trunc_idx = getelementptr i32, ptr %alloc, i128 18446744073709551616 => ptr 0x8 [alloc]
 ; CHECK-NEXT:   %gep_sext_idx = getelementptr i32, ptr %alloc, i8 -1 => ptr 0x4 [alloc + -4]
-; CHECK-NEXT:   %large_address = inttoptr i64 -1 to ptr => ptr 0xFFFFFFFFFFFFFFFF [dangling]
-; CHECK-NEXT:   %gep_update_idx_bits = getelementptr i32, ptr %large_address, i64 1 => ptr 0xFFFFFFFF00000003 [dangling]
+; CHECK-NEXT:   %large_address = inttoptr i64 -1 to ptr => ptr 0xFFFFFFFFFFFFFFFF [nullary]
+; CHECK-NEXT:   %gep_update_idx_bits = getelementptr i32, ptr %large_address, i64 1 => ptr 0xFFFFFFFF00000003 [nullary]
 ; CHECK-NEXT:   %gep_struct = getelementptr %struct, ptr %alloc_struct, i64 0, i32 1, i32 1 => ptr 0x1C [alloc_struct + 12]
-; CHECK-NEXT:   %gep_scalable_vec = getelementptr <vscale x 4 x i32>, ptr null, i64 4 => ptr 0x100 [dangling]
+; CHECK-NEXT:   %gep_scalable_vec = getelementptr <vscale x 4 x i32>, ptr null, i64 4 => ptr 0x100 [nullary]
 ; CHECK-NEXT:   %gep_vec_idx = getelementptr [2 x i32], ptr %alloc, i64 1, <2 x i64> <i64 0, i64 2> => { ptr 0x10 [alloc + 8], ptr 0x18 [alloc + 16] }
 ; CHECK-NEXT:   %ptr_vec_insert = insertelement <2 x ptr> poison, ptr %alloc, i32 0 => { ptr 0x8 [alloc], poison }
 ; CHECK-NEXT:   %ptr_vec_splat = shufflevector <2 x ptr> %ptr_vec_insert, <2 x ptr> poison, <2 x i32> zeroinitializer => { ptr 0x8 [alloc], ptr 0x8 [alloc] }
 ; CHECK-NEXT:   %gep_vec_ptr = getelementptr i32, <2 x ptr> %ptr_vec_splat, i64 1 => { ptr 0xC [alloc + 4], ptr 0xC [alloc + 4] }
 ; CHECK-NEXT:   %gep_vec_ptr_vec_idx = getelementptr i32, <2 x ptr> %ptr_vec_splat, <2 x i64> <i64 0, i64 2> => { ptr 0x8 [alloc], ptr 0x10 [alloc + 8] }
-; CHECK-NEXT:   %gep_inbounds_valid_null = getelementptr inbounds i8, ptr null, i64 0 => ptr 0x0 [dangling]
+; CHECK-NEXT:   %gep_inbounds_valid_null = getelementptr inbounds i8, ptr null, i64 0 => ptr 0x0 [nullary]
 ; CHECK-NEXT:   %gep_inbounds_invalid_null = getelementptr inbounds i8, ptr null, i64 1 => poison
 ; CHECK-NEXT:   %gep_inbounds_valid1 = getelementptr inbounds i8, ptr %alloc, i64 3 => ptr 0xB [alloc + 3]
 ; CHECK-NEXT:   %gep_inbounds_valid2 = getelementptr inbounds i8, ptr %alloc, i64 4 => ptr 0xC [alloc + 4]
@@ -120,25 +120,25 @@ define void @main() {
 ; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x28 [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 [dangling]
-; CHECK-NEXT:   %gep_inbounds_valid4 = getelementptr inbounds i8, ptr %dead_stack_ptr, i64 4 => ptr 0x2C [dangling]
+; 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:   %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
-; CHECK-NEXT:   %gep_nusw_valid2 = getelementptr nusw i32, ptr null, i32 536870911 => ptr 0x7FFFFFFC [dangling]
+; CHECK-NEXT:   %gep_nusw_valid2 = getelementptr nusw i32, ptr null, i32 536870911 => ptr 0x7FFFFFFC [nullary]
 ; CHECK-NEXT:   %gep_nusw_invalid2 = getelementptr nusw i32, ptr null, i32 536870912 => poison
-; CHECK-NEXT:   %gep_nusw_valid3 = getelementptr nusw [2 x i16], ptr null, i32 536870911, i32 1 => ptr 0x7FFFFFFE [dangling]
+; CHECK-NEXT:   %gep_nusw_valid3 = getelementptr nusw [2 x i16], ptr null, i32 536870911, i32 1 => ptr 0x7FFFFFFE [nullary]
 ; CHECK-NEXT:   %gep_nusw_invalid3 = getelementptr nusw [2 x i16], ptr null, i32 536870911, i32 2 => poison
-; CHECK-NEXT:   %large_address2 = inttoptr i64 -4 to ptr => ptr 0xFFFFFFFFFFFFFFFC [dangling]
-; CHECK-NEXT:   %gep_nusw_valid4 = getelementptr nusw i8, ptr %large_address2, i64 3 => ptr 0xFFFFFFFFFFFFFFFF [dangling]
+; CHECK-NEXT:   %large_address2 = inttoptr i64 -4 to ptr => ptr 0xFFFFFFFFFFFFFFFC [nullary]
+; CHECK-NEXT:   %gep_nusw_valid4 = getelementptr nusw i8, ptr %large_address2, i64 3 => ptr 0xFFFFFFFFFFFFFFFF [nullary]
 ; CHECK-NEXT:   %gep_nusw_invalid4 = getelementptr nusw i8, ptr %large_address2, i64 4 => poison
-; CHECK-NEXT:   %gep_nusw_valid5 = getelementptr nusw i8, ptr %large_address2, i64 -4 => ptr 0xFFFFFFFFFFFFFFF8 [dangling]
+; CHECK-NEXT:   %gep_nusw_valid5 = getelementptr nusw i8, ptr %large_address2, i64 -4 => ptr 0xFFFFFFFFFFFFFFF8 [nullary]
 ; CHECK-NEXT:   %gep_nusw_invalid5 = getelementptr nusw i8, ptr %alloc, i64 -16 => poison
-; CHECK-NEXT:   %gep_nuw_valid1 = getelementptr nuw i8, ptr null, i64 2147483647 => ptr 0x7FFFFFFF [dangling]
+; CHECK-NEXT:   %gep_nuw_valid1 = getelementptr nuw i8, ptr null, i64 2147483647 => ptr 0x7FFFFFFF [nullary]
 ; CHECK-NEXT:   %gep_nuw_invalid1 = getelementptr nuw i8, ptr null, i64 -1 => poison
-; CHECK-NEXT:   %gep_nuw_valid2 = getelementptr nuw i32, ptr null, i32 1073741823 => ptr 0xFFFFFFFC [dangling]
+; CHECK-NEXT:   %gep_nuw_valid2 = getelementptr nuw i32, ptr null, i32 1073741823 => ptr 0xFFFFFFFC [nullary]
 ; CHECK-NEXT:   %gep_nuw_invalid2 = getelementptr nuw i32, ptr null, i32 1073741824 => poison
-; CHECK-NEXT:   %gep_nuw_valid3 = getelementptr nuw [2 x i16], ptr null, i32 1073741823, i32 1 => ptr 0xFFFFFFFE [dangling]
+; CHECK-NEXT:   %gep_nuw_valid3 = getelementptr nuw [2 x i16], ptr null, i32 1073741823, i32 1 => ptr 0xFFFFFFFE [nullary]
 ; CHECK-NEXT:   %gep_nuw_invalid3 = getelementptr nuw [2 x i16], ptr null, i32 1073741823, i32 2 => poison
 ; CHECK-NEXT:   %gep_nuw_valid4 = getelementptr nuw i32, ptr %alloc, i64 1073741821 => ptr 0xFFFFFFFC [alloc + 4294967284]
 ; CHECK-NEXT:   %gep_nuw_invalid4 = getelementptr nuw i32, ptr %alloc, i64 1073741822 => poison
diff --git a/llvm/test/tools/llubi/inttoptr.ll b/llvm/test/tools/llubi/inttoptr.ll
index 4e672c38fb6fd..d29d26f69e2ad 100644
--- a/llvm/test/tools/llubi/inttoptr.ll
+++ b/llvm/test/tools/llubi/inttoptr.ll
@@ -10,8 +10,8 @@ define void @main() {
   ret void
 }
 ; CHECK: Entering function: main
-; CHECK-NEXT:   %ptr1 = inttoptr i64 0 to ptr => ptr 0x0 [dangling]
-; CHECK-NEXT:   %ptr2 = inttoptr i8 -1 to ptr => ptr 0xFF [dangling]
-; CHECK-NEXT:   %ptr3 = inttoptr i128 -1 to ptr => ptr 0xFFFFFFFFFFFFFFFF [dangling]
+; 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:   ret void
 ; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/loadstore_be.ll b/llvm/test/tools/llubi/loadstore_be.ll
index 2519a2f692a1a..d394068ffe990 100644
--- a/llvm/test/tools/llubi/loadstore_be.ll
+++ b/llvm/test/tools/llubi/loadstore_be.ll
@@ -110,6 +110,15 @@ define void @main() {
   %alloc_struct_padding = alloca {i8, i32}
   store {i8, i32} zeroinitializer, ptr %alloc_struct_padding
   %load_struct_noundef = load {i8, i32}, ptr %alloc_struct_padding, !noundef !{}
+  
+  %alloc_ptr = alloca ptr
+  store ptr %alloc_ptr, ptr %alloc_ptr
+  ; It should recover the provenance.
+  %ptr_with_provenance = load ptr, ptr %alloc_ptr
+  %addr_bits = load i8, ptr %alloc_ptr
+  store i8 %addr_bits, ptr %alloc_ptr
+  ; The first byte is tainted. We cannot recover the provenance.
+  %ptr_without_provenance = load ptr, ptr %alloc_ptr
 
   ret void
 }
@@ -137,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]
+; CHECK-NEXT:   %alloc_lifetime = alloca i32, align 4 => ptr 0xC [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
@@ -191,7 +200,10 @@ define void @main() {
 ; CHECK-NEXT:   %load_int_non_zero_padding = load i33, ptr %alloc_padding_vec, align 8 => i33 255
 ; CHECK-NEXT:   %load_vec_non_zero_padding = load <3 x i11>, ptr %alloc_padding_vec, align 8 => { i11 0, i11 0, i11 255 }
 ; CHECK-NEXT:   %alloc_struct_padding = alloca { i8, i32 }, align 8 => ptr 0x88 [alloc_struct_padding]
-; CHECK-NEXT:   store { i8, i32 } zeroinitializer, ptr %alloc_struct_padding, align 4
 ; CHECK-NEXT:   %load_struct_noundef = load { i8, i32 }, ptr %alloc_struct_padding, align 4, !noundef !0 => { i8 0, i32 0 }
+; CHECK-NEXT:   %alloc_ptr = alloca ptr, align 8 => ptr 0x88 [alloc_ptr]
+; CHECK-NEXT:   %ptr_with_provenance = load ptr, ptr %alloc_ptr, align 8 => ptr 0x88 [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 0x88 [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 84c5246d799f6..6f6826d884adc 100644
--- a/llvm/test/tools/llubi/loadstore_le.ll
+++ b/llvm/test/tools/llubi/loadstore_le.ll
@@ -111,6 +111,15 @@ define void @main() {
   %alloc_struct_padding = alloca {i8, i32}
   store {i8, i32} zeroinitializer, ptr %alloc_struct_padding
   %load_struct_noundef = load {i8, i32}, ptr %alloc_struct_padding, !noundef !{}
+  
+  %alloc_ptr = alloca ptr
+  store ptr %alloc_ptr, ptr %alloc_ptr
+  ; It should recover the provenance.
+  %ptr_with_provenance = load ptr, ptr %alloc_ptr
+  %addr_bits = load i8, ptr %alloc_ptr
+  store i8 %addr_bits, ptr %alloc_ptr
+  ; The first byte is tainted. We cannot recover the provenance.
+  %ptr_without_provenance = load ptr, ptr %alloc_ptr
 
   ret void
 }
@@ -139,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]
+; CHECK-NEXT:   %alloc_lifetime = alloca i32, align 4 => ptr 0xC [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
@@ -195,5 +204,11 @@ define void @main() {
 ; CHECK-NEXT:   %alloc_struct_padding = alloca { i8, i32 }, align 8 => ptr 0x88 [alloc_struct_padding]
 ; CHECK-NEXT:   store { i8, i32 } zeroinitializer, ptr %alloc_struct_padding, align 4
 ; CHECK-NEXT:   %load_struct_noundef = load { i8, i32 }, ptr %alloc_struct_padding, align 4, !noundef !0 => { i8 0, i32 0 }
+; CHECK-NEXT:   %alloc_ptr = alloca ptr, align 8 => ptr 0x88 [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 0x88 [alloc_ptr]
+; CHECK-NEXT:   %addr_bits = load i8, ptr %alloc_ptr, align 1 => i8 -120
+; CHECK-NEXT:   store i8 %addr_bits, ptr %alloc_ptr, align 1
+; CHECK-NEXT:   %ptr_without_provenance = load ptr, ptr %alloc_ptr, align 8 => ptr 0x88 [nullary]
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/loadstore_uaf.ll b/llvm/test/tools/llubi/loadstore_uaf.ll
index ab81a75fd2cb7..a4593bff99c79 100644
--- a/llvm/test/tools/llubi/loadstore_uaf.ll
+++ b/llvm/test/tools/llubi/loadstore_uaf.ll
@@ -16,7 +16,7 @@ define void @main() {
 ; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x8 [alloc]
 ; CHECK-NEXT:   ret ptr %alloc
 ; CHECK-NEXT: Exiting function: stack_object
-; CHECK-NEXT:   %alloc = call ptr @stack_object() => ptr 0x8 [dangling]
+; CHECK-NEXT:   %alloc = call ptr @stack_object() => ptr 0x8 [alloc (dangling)]
 ; CHECK-NEXT: Stacktrace:
 ; CHECK-NEXT: #0   {{store i32 0|%res = load i32}}, ptr %alloc, align 4 at @main
 ; CHECK-NEXT: Immediate UB detected: Try to access a dead memory object at address 0x8.
diff --git a/llvm/test/tools/llubi/store_dead.ll b/llvm/test/tools/llubi/store_dead.ll
index 53ad2525369cb..e1151ec8d5b74 100644
--- a/llvm/test/tools/llubi/store_dead.ll
+++ b/llvm/test/tools/llubi/store_dead.ll
@@ -10,7 +10,7 @@ define void @main() {
   ret void
 }
 ; CHECK: Entering function: main
-; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x8 [alloc]
+; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x8 [alloc (dead)]
 ; CHECK-NEXT:   call void @llvm.lifetime.start.p0(ptr %alloc)
 ; CHECK-NEXT:   store i32 0, ptr %alloc, align 4
 ; CHECK-NEXT:   call void @llvm.lifetime.end.p0(ptr %alloc)
diff --git a/llvm/tools/llubi/lib/Context.cpp b/llvm/tools/llubi/lib/Context.cpp
index 5b81b068460de..b4a2d077e4cc1 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -110,6 +110,24 @@ const AnyValue &Context::getConstantValue(Constant *C) {
   return ConstCache.emplace(C, getConstantValueImpl(C)).first->second;
 }
 
+APInt Context::getTag(uint32_t BitWidth, MemoryObject *Obj) {
+  // Nullary provenance.
+  if (!Obj)
+    return APInt::getZero(BitWidth);
+  // The tag is already initialized.
+  if (!Obj->getTag().isZero())
+    return Obj->getTag();
+
+  // FIXME: This doesn't work when the address space is too small.
+  while (true) {
+    APInt Tag = generateRandomAPInt(BitWidth);
+    if (Tag.isZero() || !CapturedMemoryObjects.try_emplace(Tag, Obj).second)
+      continue;
+    Obj->setTag(Tag);
+    return Tag;
+  }
+}
+
 AnyValue Context::fromBytes(ConstBytesView Bytes, Type *Ty,
                             uint32_t OffsetInBits, bool CheckPaddingBits,
                             bool *ContainsUndefinedBits) {
@@ -119,7 +137,12 @@ AnyValue Context::fromBytes(ConstBytesView Bytes, Type *Ty,
     NewOffsetInBits = alignTo(NewOffsetInBits, 8);
   bool NeedsPadding = NewOffsetInBits != OffsetInBits + NumBits;
   uint32_t NumBitsToExtract = NewOffsetInBits - OffsetInBits;
-  SmallVector<uint64_t> RawBits(alignTo(NumBitsToExtract, 8));
+  uint32_t NumWords = divideCeil(NumBitsToExtract, 8);
+  SmallVector<uint64_t> RawBits(NumWords);
+  bool IsTagValid = Ty->isPointerTy();
+  SmallVector<uint64_t> RawTagBits;
+  if (Ty->isPointerTy())
+    RawTagBits.resize(NumWords);
   for (uint32_t I = 0; I < NumBitsToExtract; I += 8) {
     // Try to form a 'logical' byte that represents the bits in the range
     // [BitsStart, BitsEnd].
@@ -159,6 +182,15 @@ AnyValue Context::fromBytes(ConstBytesView Bytes, Type *Ty,
                           (RandomBits & ~LogicalByte.ConcreteMask)) &
                          Mask;
     RawBits[I / 64] |= static_cast<APInt::WordType>(ActualBits) << (I % 64);
+    if (IsTagValid) {
+      if ((LogicalByte.TagMask & LogicalByte.ConcreteMask) == Mask) {
+        uint8_t ActualTagBits = LogicalByte.TagValue & Mask;
+        RawTagBits[I / 64] |= static_cast<APInt::WordType>(ActualTagBits)
+                              << (I % 64);
+      } else {
+        IsTagValid = false;
+      }
+    }
   }
   OffsetInBits = NewOffsetInBits;
 
@@ -179,7 +211,12 @@ AnyValue Context::fromBytes(ConstBytesView Bytes, Type *Ty,
   if (Ty->isFloatingPointTy())
     return APFloat(Ty->getFltSemantics(), Bits);
   assert(Ty->isPointerTy() && "Expect a pointer type");
-  // TODO: recover provenance
+  // Try to recover provenance from the tag.
+  if (IsTagValid) {
+    // TODO: decode metadata bits from the tag.
+    APInt Tag(NumBitsToExtract, RawTagBits);
+    return Pointer(CapturedMemoryObjects.lookup(Tag), Bits);
+  }
   return Pointer(Bits);
 }
 
@@ -259,7 +296,7 @@ void Context::toBytes(const AnyValue &Val, Type *Ty, uint32_t OffsetInBits,
   if (PaddingBits)
     NewOffsetInBits = alignTo(NewOffsetInBits, 8);
   bool NeedsPadding = NewOffsetInBits != OffsetInBits + NumBits;
-  auto WriteBits = [&](const APInt &Bits) {
+  auto WriteBits = [&](const APInt &Bits, const APInt *TagBits) {
     for (uint32_t I = 0, E = Bits.getBitWidth(); I < E; I += 8) {
       uint32_t NumBitsInByte = std::min(8U, E - I);
       uint32_t BitsStart = OffsetInBits + I;
@@ -276,6 +313,21 @@ void Context::toBytes(const AnyValue &Val, Type *Ty, uint32_t OffsetInBits,
         Bytes[BitsEnd / 8].writeBits(
             static_cast<uint8_t>((1U << (BitsEnd % 8 + 1)) - 1),
             static_cast<uint8_t>(BitsVal >> (8 - (BitsStart % 8))));
+
+      if (TagBits) {
+        uint8_t TagBitsVal = static_cast<uint8_t>(
+            TagBits->extractBitsAsZExtValue(NumBitsInByte, I));
+        Bytes[BitsStart / 8].writeTagBits(
+            static_cast<uint8_t>(((1U << NumBitsInByte) - 1)
+                                 << (BitsStart % 8)),
+            static_cast<uint8_t>(TagBitsVal << (BitsStart % 8)));
+        // If it is a cross-byte access, write the remaining bits to the next
+        // byte.
+        if (((BitsStart ^ BitsEnd) & ~7) != 0)
+          Bytes[BitsEnd / 8].writeTagBits(
+              static_cast<uint8_t>((1U << (BitsEnd % 8 + 1)) - 1),
+              static_cast<uint8_t>(TagBitsVal >> (8 - (BitsStart % 8))));
+      }
     }
   };
   if (Val.isPoison()) {
@@ -289,14 +341,26 @@ void Context::toBytes(const AnyValue &Val, Type *Ty, uint32_t OffsetInBits,
     }
   } else if (Ty->isIntegerTy()) {
     auto &Bits = Val.asInteger();
-    WriteBits(NeedsPadding ? Bits.zext(NewOffsetInBits - OffsetInBits) : Bits);
+    WriteBits(NeedsPadding ? Bits.zext(NewOffsetInBits - OffsetInBits) : Bits,
+              /*TagBits=*/nullptr);
   } else if (Ty->isFloatingPointTy()) {
     auto Bits = Val.asFloat().bitcastToAPInt();
-    WriteBits(NeedsPadding ? Bits.zext(NewOffsetInBits - OffsetInBits) : Bits);
+    WriteBits(NeedsPadding ? Bits.zext(NewOffsetInBits - OffsetInBits) : Bits,
+              /*TagBits=*/nullptr);
   } else if (Ty->isPointerTy()) {
-    auto &Bits = Val.asPointer().address();
-    WriteBits(NeedsPadding ? Bits.zext(NewOffsetInBits - OffsetInBits) : Bits);
-    // TODO: save metadata of the pointer.
+    auto &AddressBits = Val.asPointer().address();
+    if (auto *MO = Val.asPointer().getMemoryObject()) {
+      APInt Tag = getTag(AddressBits.getBitWidth(), MO);
+      if (NeedsPadding)
+        Tag = Tag.zext(NewOffsetInBits - OffsetInBits);
+      WriteBits(NeedsPadding ? AddressBits.zext(NewOffsetInBits - OffsetInBits)
+                             : AddressBits,
+                &Tag);
+    } else {
+      WriteBits(NeedsPadding ? AddressBits.zext(NewOffsetInBits - OffsetInBits)
+                             : AddressBits,
+                /*TagBits=*/nullptr);
+    }
   } else {
     llvm_unreachable("Unsupported scalar type.");
   }
@@ -392,21 +456,24 @@ void Context::storeRawBytes(MemoryObject &MO, uint64_t Offset, const void *Data,
     MO[Offset + I] = Byte::concrete(static_cast<const uint8_t *>(Data)[I]);
 }
 
+APInt Context::generateRandomAPInt(uint32_t BitWidth) {
+  SmallVector<APInt::WordType> RandomWords;
+  uint32_t NumWords = APInt::getNumWords(BitWidth);
+  RandomWords.reserve(NumWords);
+  static_assert(decltype(Rng)::word_size >=
+                    std::numeric_limits<APInt::WordType>::digits,
+                "Unexpected Rng result type.");
+  for (uint32_t I = 0; I != NumWords; ++I)
+    RandomWords.push_back(static_cast<APInt::WordType>(Rng()));
+  return APInt(BitWidth, RandomWords);
+}
+
 void Context::freeze(AnyValue &Val, Type *Ty) {
   if (Val.isPoison()) {
     uint32_t Bits = DL.getTypeSizeInBits(Ty);
-    APInt RandomVal = APInt::getZero(Bits);
-    if (UndefBehavior == UndefValueBehavior::NonDeterministic) {
-      SmallVector<APInt::WordType> RandomWords;
-      uint32_t NumWords = APInt::getNumWords(Bits);
-      RandomWords.reserve(NumWords);
-      static_assert(decltype(Rng)::word_size >=
-                        std::numeric_limits<APInt::WordType>::digits,
-                    "Unexpected Rng result type.");
-      for (uint32_t I = 0; I != NumWords; ++I)
-        RandomWords.push_back(static_cast<APInt::WordType>(Rng()));
-      RandomVal = APInt(Bits, RandomWords);
-    }
+    APInt RandomVal = UndefBehavior == UndefValueBehavior::NonDeterministic
+                          ? generateRandomAPInt(Bits)
+                          : APInt::getZero(Bits);
     if (Ty->isIntegerTy())
       Val = AnyValue(RandomVal);
     else if (Ty->isFloatingPointTy())
diff --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index aa9d61cc6077e..1ad0fedddac16 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -94,6 +94,16 @@ class MemoryObject : public RefCountedBase<MemoryObject> {
   MemAllocKind AllocKind;
   bool IsConstant = false;
 
+  // A tag is a randomly generated unique identifier to recover the provenance
+  // of a pointer. The length of tag is equal to the store size of the pointer
+  // 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: allow encoding metadata bits (e.g., captured pointer components) into
+  // the tag. The injection should affect all the bits in the tag rather than
+  // just some low bits for better robustness.
+  APInt Tag;
+
 public:
   MemoryObject(uint64_t Addr, uint64_t Size, StringRef Name, unsigned AS,
                MemInitKind InitKind, MemAllocKind AllocKind);
@@ -112,6 +122,8 @@ class MemoryObject : public RefCountedBase<MemoryObject> {
   MemAllocKind getAllocKind() const { return AllocKind; }
   bool isConstant() const { return IsConstant; }
   void setIsConstant(bool C) { IsConstant = C; }
+  const APInt &getTag() const { return Tag; }
+  void setTag(const APInt &T) { Tag = T; }
 
   bool inBounds(const APInt &NewAddr) const {
     return NewAddr.uge(Address) && NewAddr.ule(Address + Size);
@@ -195,6 +207,7 @@ class Context {
   UndefValueBehavior UndefBehavior = UndefValueBehavior::NonDeterministic;
 
   std::mt19937_64 Rng;
+  APInt generateRandomAPInt(uint32_t BitWidth);
 
   // Memory
   uint64_t UsedMem = 0;
@@ -202,14 +215,18 @@ class Context {
   // For now we don't model the behavior of address reuse, which is common
   // with stack coloring.
   uint64_t AllocationBase = 8;
-  // Maintains a global list of 'exposed' provenances. This is used to form a
-  // pointer with an exposed provenance.
-  // FIXME: Currently all the allocations are considered exposed, regardless of
-  // their interaction with ptrtoint. That is, ptrtoint is allowed to recover
-  // the provenance of any allocation. We may track the exposed provenances more
-  // precisely after we make ptrtoint have the implicit side-effect of exposing
-  // the provenance.
-  std::map<uint64_t, IntrusiveRefCntPtr<MemoryObject>> MemoryObjects;
+  // All live memory objects.
+  // FIXME: global objects and stack objects should not be tracked in this map.
+  DenseMap<uint64_t, IntrusiveRefCntPtr<MemoryObject>> MemoryObjects;
+  // Mapping from tags to memory objects. Tags are lazily generated when a
+  // pointer is captured.
+  DenseMap<APInt, IntrusiveRefCntPtr<MemoryObject>> CapturedMemoryObjects;
+  // TODO: Maintains a global list of 'exposed' provenances. This is used to
+  // convert an address back to a pointer with a previously exposed provenance.
+
+  /// Get the tag for a pointer to the given memory object.
+  /// TODO: encode metadata bits into the tag.
+  APInt getTag(uint32_t BitWidth, MemoryObject *Obj);
   AnyValue fromBytes(ConstBytesView Bytes, Type *Ty, uint32_t OffsetInBits,
                      bool CheckPaddingBits, bool *ContainsUndefinedBits);
   void toBytes(const AnyValue &Val, Type *Ty, uint32_t OffsetInBits,
diff --git a/llvm/tools/llubi/lib/Value.cpp b/llvm/tools/llubi/lib/Value.cpp
index 82bf0f7b6eb22..1bc835b662b51 100644
--- a/llvm/tools/llubi/lib/Value.cpp
+++ b/llvm/tools/llubi/lib/Value.cpp
@@ -20,13 +20,15 @@ void Pointer::print(raw_ostream &OS) const {
   SmallString<32> AddrStr;
   Address.toStringUnsigned(AddrStr, 16);
   OS << "ptr 0x" << AddrStr << " [";
-  if (Obj && Obj->getState() != MemoryObjectState::Freed) {
+  if (Obj) {
     OS << Obj->getName();
-    // TODO: print " (dead)" if the stack object is out of lifetime.
     if (Address != Obj->getAddress())
       OS << " + " << (Address - Obj->getAddress());
+    MemoryObjectState State = Obj->getState();
+    if (State != MemoryObjectState::Alive)
+      OS << (State == MemoryObjectState::Dead ? " (dead)" : " (dangling)");
   } else {
-    OS << "dangling";
+    OS << "nullary";
   }
   OS << "]";
 }
diff --git a/llvm/tools/llubi/lib/Value.h b/llvm/tools/llubi/lib/Value.h
index dfaf5f23a15b0..59d9a5c5f46fd 100644
--- a/llvm/tools/llubi/lib/Value.h
+++ b/llvm/tools/llubi/lib/Value.h
@@ -27,15 +27,18 @@ class AnyValue;
 /// - If the concrete mask bit is 0, the bit is either undef or poison. The
 /// value bit indicates whether it is undef.
 /// - If the concrete mask bit is 1, the bit is a concrete value. The value bit
-/// stores the concrete bit value.
+/// stores the concrete bit value. The tag mask bit indicates whether it is a
+/// pointer bit, and the tag value bit is used for provenance tracking of
+/// pointers.
 struct Byte {
   uint8_t ConcreteMask;
   uint8_t Value;
-  // TODO: captured capabilities of pointers.
+  uint8_t TagMask;  // A mask to indicate which bits are pointer bits.
+  uint8_t TagValue; // Part of the tag for provenance tracking of pointers.
 
-  static Byte poison() { return Byte{0, 0}; }
-  static Byte undef() { return Byte{0, 255}; }
-  static Byte concrete(uint8_t Val) { return Byte{255, Val}; }
+  static Byte poison() { return Byte{0, 0, 0, 0}; }
+  static Byte undef() { return Byte{0, 255, 0, 0}; }
+  static Byte concrete(uint8_t Val) { return Byte{255, Val, 0, 0}; }
 
   void zeroBits(uint8_t Mask) {
     ConcreteMask |= Mask;
@@ -55,6 +58,15 @@ struct Byte {
   void writeBits(uint8_t Mask, uint8_t Val) {
     ConcreteMask |= Mask;
     Value = (Value & ~Mask) | (Val & Mask);
+    TagMask &= ~Mask;
+  }
+
+  void writeTagBits(uint8_t Mask, uint8_t Tag) {
+    assert(
+        (ConcreteMask & Mask) == Mask &&
+        "Please ensure pointer bits are concrete before calling writeTagBits.");
+    TagMask |= Mask;
+    TagValue = (TagValue & ~Mask) | (Tag & Mask);
   }
 
   /// Returns a logical byte that is part of two adjacent bytes.
@@ -63,14 +75,19 @@ struct Byte {
   /// LSB | 0 1 0 1 0 1 0 1 | 0 0 0 0 1 1 1 1 | MSB
   ///     Result =  | 1 0 1   0 0 0 0 1 |
   static Byte fshr(const Byte &Low, const Byte &High, uint32_t ShAmt) {
-    return Byte{static_cast<uint8_t>(
-                    (Low.ConcreteMask | (High.ConcreteMask << 8)) >> ShAmt),
-                static_cast<uint8_t>((Low.Value | (High.Value << 8)) >> ShAmt)};
+    return Byte{
+        static_cast<uint8_t>((Low.ConcreteMask | (High.ConcreteMask << 8)) >>
+                             ShAmt),
+        static_cast<uint8_t>((Low.Value | (High.Value << 8)) >> ShAmt),
+        static_cast<uint8_t>((Low.TagMask | (High.TagMask << 8)) >> ShAmt),
+        static_cast<uint8_t>((Low.TagValue | (High.TagValue << 8)) >> ShAmt)};
   }
 
   Byte lshr(uint8_t Shift) const {
     return Byte{static_cast<uint8_t>(ConcreteMask >> Shift),
-                static_cast<uint8_t>(Value >> Shift)};
+                static_cast<uint8_t>(Value >> Shift),
+                static_cast<uint8_t>(TagMask >> Shift),
+                static_cast<uint8_t>(TagValue >> Shift)};
   }
 };
 

>From c0eb8862d8262a0017293d75185d4915706f9cd8 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Wed, 18 Mar 2026 20:59:24 +0800
Subject: [PATCH 2/6] [llubi] Add some comments about wildcard provenance

---
 llvm/tools/llubi/lib/Context.h | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index 1ad0fedddac16..96b30f1382197 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -102,6 +102,8 @@ class MemoryObject : public RefCountedBase<MemoryObject> {
   // TODO: allow encoding metadata bits (e.g., captured pointer components) into
   // the tag. The injection should affect all the bits in the tag rather than
   // just some low bits for better robustness.
+  // TODO: we need a special tag encoding for wildcard provenance, which is
+  // introduced by inttoptr.
   APInt Tag;
 
 public:

>From a92943d5c40147520e60bf13b7b0fa8cd3f8669b Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Sun, 3 May 2026 16:54:07 +0800
Subject: [PATCH 3/6] [llubi] Update tests.

---
 llvm/test/tools/llubi/lib_calloc_large_size.ll    | 4 ++--
 llvm/test/tools/llubi/lib_calloc_size_overflow.ll | 2 +-
 llvm/test/tools/llubi/lib_free_nullary_pointer.ll | 2 +-
 llvm/test/tools/llubi/lib_malloc_large_size.ll    | 4 ++--
 4 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/llvm/test/tools/llubi/lib_calloc_large_size.ll b/llvm/test/tools/llubi/lib_calloc_large_size.ll
index 72b0764185a47..1a7968bc8d373 100644
--- a/llvm/test/tools/llubi/lib_calloc_large_size.ll
+++ b/llvm/test/tools/llubi/lib_calloc_large_size.ll
@@ -12,7 +12,7 @@ entry:
 }
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %ptr_1 = call ptr @calloc(i64 2, i64 25) => ptr 0x10 [ptr_1]
-; CHECK-NEXT:   %ptr_2 = call ptr @calloc(i64 4, i64 25) => ptr 0x0 [dangling]
-; CHECK-NEXT:   %ptr_3 = call ptr @calloc(i64 8, i64 25) => ptr 0x0 [dangling]
+; CHECK-NEXT:   %ptr_2 = call ptr @calloc(i64 4, i64 25) => ptr 0x0 [nullary]
+; CHECK-NEXT:   %ptr_3 = call ptr @calloc(i64 8, i64 25) => ptr 0x0 [nullary]
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/lib_calloc_size_overflow.ll b/llvm/test/tools/llubi/lib_calloc_size_overflow.ll
index ea06f2bde1018..21446f0c62a89 100644
--- a/llvm/test/tools/llubi/lib_calloc_size_overflow.ll
+++ b/llvm/test/tools/llubi/lib_calloc_size_overflow.ll
@@ -9,6 +9,6 @@ entry:
   ret void
 }
 ; CHECK: Entering function: main
-; CHECK-NEXT:   %ptr = call ptr @calloc(i64 -1, i64 2) => ptr 0x0 [dangling]
+; CHECK-NEXT:   %ptr = call ptr @calloc(i64 -1, i64 2) => ptr 0x0 [nullary]
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/lib_free_nullary_pointer.ll b/llvm/test/tools/llubi/lib_free_nullary_pointer.ll
index 251608a547ed9..098e387c70825 100644
--- a/llvm/test/tools/llubi/lib_free_nullary_pointer.ll
+++ b/llvm/test/tools/llubi/lib_free_nullary_pointer.ll
@@ -10,7 +10,7 @@ define i32 @main() {
   ret i32 0
 }
 ; CHECK: Entering function: main
-; CHECK-NEXT:   %p = getelementptr i8, ptr null, i64 42 => ptr 0x2A [dangling]
+; CHECK-NEXT:   %p = getelementptr i8, ptr null, i64 42 => ptr 0x2A [nullary]
 ; CHECK-NEXT: Stacktrace:
 ; CHECK-NEXT: #0   call void @free(ptr %p) at @main
 ; CHECK-NEXT: Immediate UB detected: freeing a pointer with nullary provenance.
diff --git a/llvm/test/tools/llubi/lib_malloc_large_size.ll b/llvm/test/tools/llubi/lib_malloc_large_size.ll
index 8a1800f53af92..483290e11fea7 100644
--- a/llvm/test/tools/llubi/lib_malloc_large_size.ll
+++ b/llvm/test/tools/llubi/lib_malloc_large_size.ll
@@ -12,7 +12,7 @@ entry:
 }
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %ptr_1 = call ptr @malloc(i64 50) => ptr 0x10 [ptr_1]
-; CHECK-NEXT:   %ptr_2 = call ptr @malloc(i64 100) => ptr 0x0 [dangling]
-; CHECK-NEXT:   %ptr_3 = call ptr @malloc(i64 200) => ptr 0x0 [dangling]
+; CHECK-NEXT:   %ptr_2 = call ptr @malloc(i64 100) => ptr 0x0 [nullary]
+; CHECK-NEXT:   %ptr_3 = call ptr @malloc(i64 200) => ptr 0x0 [nullary]
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: main

>From e9531437cbb2b38c2f1881744ae61365de12133b Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Mon, 4 May 2026 23:17:09 +0800
Subject: [PATCH 4/6] [llubi] Update tests.

---
 .../tools/llubi/assume_misalign_all_ones.ll   |  2 +-
 llvm/test/tools/llubi/assume_null.ll          |  2 +-
 llvm/test/tools/llubi/assume_null_all_ones.ll |  4 +--
 ...e_dereferenceable_ub_nullary_provenance.ll |  4 +--
 llvm/test/tools/llubi/attributes.ll           | 26 +++++++++----------
 llvm/test/tools/llubi/loadstore_be.ll         | 11 +++++---
 llvm/test/tools/llubi/loadstore_le.ll         | 10 +++----
 llvm/test/tools/llubi/metadata.ll             | 20 +++++++-------
 8 files changed, 40 insertions(+), 39 deletions(-)

diff --git a/llvm/test/tools/llubi/assume_misalign_all_ones.ll b/llvm/test/tools/llubi/assume_misalign_all_ones.ll
index aaf54f892789f..c3e96fb144cbd 100644
--- a/llvm/test/tools/llubi/assume_misalign_all_ones.ll
+++ b/llvm/test/tools/llubi/assume_misalign_all_ones.ll
@@ -10,5 +10,5 @@ define void @main() {
 ; CHECK: Entering function: main
 ; CHECK-NEXT: Stacktrace:
 ; CHECK-NEXT: #0   call void @llvm.assume(i1 true) [ "align"(ptr addrspace(1) null, i32 2048) ] at @main
-; CHECK-NEXT: Immediate UB detected: The pointer ptr 0xFFFFFFFFFFFFFFFF [dangling] violates align(2048) assumption.
+; CHECK-NEXT: Immediate UB detected: The pointer ptr 0xFFFFFFFFFFFFFFFF [nullary] violates align(2048) assumption.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/assume_null.ll b/llvm/test/tools/llubi/assume_null.ll
index ef55ce54ed6b7..be187dd166a58 100644
--- a/llvm/test/tools/llubi/assume_null.ll
+++ b/llvm/test/tools/llubi/assume_null.ll
@@ -8,5 +8,5 @@ define void @main() {
 ; CHECK: Entering function: main
 ; CHECK-NEXT: Stacktrace:
 ; CHECK-NEXT: #0   call void @llvm.assume(i1 true) [ "nonnull"(ptr null) ] at @main
-; CHECK-NEXT: Immediate UB detected: The pointer ptr 0x0 [dangling] violates nonnull assumption.
+; CHECK-NEXT: Immediate UB detected: The pointer ptr 0x0 [nullary] violates nonnull assumption.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/assume_null_all_ones.ll b/llvm/test/tools/llubi/assume_null_all_ones.ll
index 94dcebb600056..d8663f43c2116 100644
--- a/llvm/test/tools/llubi/assume_null_all_ones.ll
+++ b/llvm/test/tools/llubi/assume_null_all_ones.ll
@@ -13,8 +13,8 @@ define void @main() {
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %storage = alloca ptr, align 8 => ptr 0x8 [storage]
 ; CHECK-NEXT:   store i64 -1, ptr %storage, align 4
-; CHECK-NEXT:   %res = load ptr addrspace(1), ptr %storage, align 8 => ptr 0xFFFFFFFFFFFFFFFF [dangling]
+; CHECK-NEXT:   %res = load ptr addrspace(1), ptr %storage, align 8 => ptr 0xFFFFFFFFFFFFFFFF [nullary]
 ; CHECK-NEXT: Stacktrace:
 ; CHECK-NEXT: #0   call void @llvm.assume(i1 true) [ "nonnull"(ptr addrspace(1) %res) ] at @main
-; CHECK-NEXT: Immediate UB detected: The pointer ptr 0xFFFFFFFFFFFFFFFF [dangling] violates nonnull assumption.
+; CHECK-NEXT: Immediate UB detected: The pointer ptr 0xFFFFFFFFFFFFFFFF [nullary] violates nonnull assumption.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/attribute_dereferenceable_ub_nullary_provenance.ll b/llvm/test/tools/llubi/attribute_dereferenceable_ub_nullary_provenance.ll
index b4d585c2a71c3..7d3e4edf6ae8e 100644
--- a/llvm/test/tools/llubi/attribute_dereferenceable_ub_nullary_provenance.ll
+++ b/llvm/test/tools/llubi/attribute_dereferenceable_ub_nullary_provenance.ll
@@ -13,8 +13,8 @@ define void @main() {
 }
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %ptr_storage = alloca i64, align 8 => ptr 0x8 [ptr_storage]
-; CHECK-NEXT:   %p = load ptr, ptr %ptr_storage, align 8 => ptr 0xE82FEEACEEB98B3E [dangling]
+; CHECK-NEXT:   %p = load ptr, ptr %ptr_storage, align 8 => ptr 0xE82FEEACEEB98B3E [nullary]
 ; CHECK-NEXT: Stacktrace:
 ; CHECK-NEXT: #0   call void @callee(ptr %p) at @main
-; CHECK-NEXT: Immediate UB detected: The value ptr 0xE82FEEACEEB98B3E [dangling] violates dereferenceable{{(_or_null)?}}(4) attribute.
+; CHECK-NEXT: Immediate UB detected: The value ptr 0xE82FEEACEEB98B3E [nullary] violates dereferenceable{{(_or_null)?}}(4) attribute.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/attributes.ll b/llvm/test/tools/llubi/attributes.ll
index 1a4acfb76a0c2..9ec65e31c8390 100644
--- a/llvm/test/tools/llubi/attributes.ll
+++ b/llvm/test/tools/llubi/attributes.ll
@@ -197,7 +197,7 @@ define void @main() {
 ; CHECK-NEXT: Exiting function: identity_nofpclass_agg
 ; CHECK-NEXT:   %nofpclass_agg = call { <2 x half>, <2 x half> } @identity_nofpclass_agg({ <2 x half>, <2 x half> } { <2 x half> <half 0xH3C00, half poison>, <2 x half> <half 0xH7C00, half 0xH7E00> }) => { { half 1.000000e+00, poison }, { poison, poison } }
 ; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x8 [alloc]
-; CHECK-NEXT:   %ptr_one = getelementptr i8, ptr null, i32 1 => ptr 0x1 [dangling]
+; CHECK-NEXT:   %ptr_one = getelementptr i8, ptr null, i32 1 => ptr 0x1 [nullary]
 ; CHECK-NEXT: Entering function: gep_nonnull
 ; CHECK-NEXT:   ptr %p = ptr 0x8 [alloc]
 ; CHECK-NEXT:   %gep = getelementptr i8, ptr %p, i32 -1 => ptr 0x7 [alloc + -1]
@@ -211,8 +211,8 @@ define void @main() {
 ; CHECK-NEXT: Exiting function: gep_nonnull
 ; CHECK-NEXT:   %nonnull_invalid_input = call ptr @gep_nonnull(ptr null) => poison
 ; CHECK-NEXT: Entering function: gep_nonnull
-; CHECK-NEXT:   ptr %p = ptr 0x1 [dangling]
-; CHECK-NEXT:   %gep = getelementptr i8, ptr %p, i32 -1 => ptr 0x0 [dangling]
+; CHECK-NEXT:   ptr %p = ptr 0x1 [nullary]
+; CHECK-NEXT:   %gep = getelementptr i8, ptr %p, i32 -1 => ptr 0x0 [nullary]
 ; CHECK-NEXT:   ret ptr %gep
 ; CHECK-NEXT: Exiting function: gep_nonnull
 ; CHECK-NEXT:   %nonnull_invalid_output = call ptr @gep_nonnull(ptr %ptr_one) => poison
@@ -229,8 +229,8 @@ define void @main() {
 ; CHECK-NEXT: Exiting function: gep
 ; CHECK-NEXT:   %nonnull_callsite_invalid_input = call ptr @gep(ptr nonnull null) => poison
 ; CHECK-NEXT: Entering function: gep
-; CHECK-NEXT:   ptr %p = ptr 0x1 [dangling]
-; CHECK-NEXT:   %gep = getelementptr i8, ptr %p, i32 -1 => ptr 0x0 [dangling]
+; CHECK-NEXT:   ptr %p = ptr 0x1 [nullary]
+; CHECK-NEXT:   %gep = getelementptr i8, ptr %p, i32 -1 => ptr 0x0 [nullary]
 ; CHECK-NEXT:   ret ptr %gep
 ; CHECK-NEXT: Exiting function: gep
 ; CHECK-NEXT:   %nonnull_callsite_invalid_output = call nonnull ptr @gep(ptr %ptr_one) => poison
@@ -247,17 +247,17 @@ define void @main() {
 ; CHECK-NEXT: Exiting function: gep_align
 ; CHECK-NEXT:   %align_invalid_input = call ptr @gep_align(ptr %ptr_one) => poison
 ; CHECK-NEXT: Entering function: gep_align
-; CHECK-NEXT:   ptr %p = ptr 0x0 [dangling]
-; CHECK-NEXT:   %gep = getelementptr i8, ptr %p, i32 8 => ptr 0x8 [dangling]
+; CHECK-NEXT:   ptr %p = ptr 0x0 [nullary]
+; CHECK-NEXT:   %gep = getelementptr i8, ptr %p, i32 8 => ptr 0x8 [nullary]
 ; CHECK-NEXT:   ret ptr %gep
 ; CHECK-NEXT: Exiting function: gep_align
 ; CHECK-NEXT:   %align_invalid_output = call ptr @gep_align(ptr null) => poison
 ; CHECK-NEXT:   %ptr_vec_1 = insertelement <4 x ptr> poison, ptr %alloc, i32 0 => { ptr 0x8 [alloc], poison, poison, poison }
-; CHECK-NEXT:   %ptr_vec_2 = insertelement <4 x ptr> %ptr_vec_1, ptr %ptr_one, i32 1 => { ptr 0x8 [alloc], ptr 0x1 [dangling], poison, poison }
-; CHECK-NEXT:   %ptr_vec_3 = insertelement <4 x ptr> %ptr_vec_2, ptr null, i32 2 => { ptr 0x8 [alloc], ptr 0x1 [dangling], ptr 0x0 [dangling], poison }
+; CHECK-NEXT:   %ptr_vec_2 = insertelement <4 x ptr> %ptr_vec_1, ptr %ptr_one, i32 1 => { ptr 0x8 [alloc], ptr 0x1 [nullary], poison, poison }
+; CHECK-NEXT:   %ptr_vec_3 = insertelement <4 x ptr> %ptr_vec_2, ptr null, i32 2 => { ptr 0x8 [alloc], ptr 0x1 [nullary], ptr 0x0 [nullary], poison }
 ; CHECK-NEXT: Entering function: gep_align_vec
-; CHECK-NEXT:   <4 x ptr> %p = { ptr 0x8 [alloc], poison, ptr 0x0 [dangling], poison }
-; CHECK-NEXT:   %gep = getelementptr i8, <4 x ptr> %p, i32 8 => { ptr 0x10 [alloc + 8], poison, ptr 0x8 [dangling], poison }
+; CHECK-NEXT:   <4 x ptr> %p = { ptr 0x8 [alloc], poison, ptr 0x0 [nullary], poison }
+; CHECK-NEXT:   %gep = getelementptr i8, <4 x ptr> %p, i32 8 => { ptr 0x10 [alloc + 8], poison, ptr 0x8 [nullary], poison }
 ; CHECK-NEXT:   ret <4 x ptr> %gep
 ; CHECK-NEXT: Exiting function: gep_align_vec
 ; CHECK-NEXT:   %align_vec = call <4 x ptr> @gep_align_vec(<4 x ptr> %ptr_vec_3) => { ptr 0x10 [alloc + 8], poison, poison, poison }
@@ -311,10 +311,10 @@ define void @main() {
 ; CHECK-NEXT: Exiting function: identity_dereferenceable_or_null
 ; CHECK-NEXT:   %deref_or_null_valid1 = call ptr @identity_dereferenceable_or_null(ptr %alloc) => ptr 0x8 [alloc]
 ; CHECK-NEXT: Entering function: identity_dereferenceable_or_null
-; CHECK-NEXT:   ptr %p = ptr 0x0 [dangling]
+; CHECK-NEXT:   ptr %p = ptr 0x0 [nullary]
 ; CHECK-NEXT:   ret ptr %p
 ; CHECK-NEXT: Exiting function: identity_dereferenceable_or_null
-; CHECK-NEXT:   %deref_or_null_valid2 = call ptr @identity_dereferenceable_or_null(ptr null) => ptr 0x0 [dangling]
+; CHECK-NEXT:   %deref_or_null_valid2 = call ptr @identity_dereferenceable_or_null(ptr null) => ptr 0x0 [nullary]
 ; CHECK-NEXT: Entering function: identity_dereferenceable_or_null
 ; CHECK-NEXT:   ptr %p = ptr 0x8 [alloc]
 ; CHECK-NEXT:   ret ptr %p
diff --git a/llvm/test/tools/llubi/loadstore_be.ll b/llvm/test/tools/llubi/loadstore_be.ll
index d394068ffe990..8f2e0815f88fe 100644
--- a/llvm/test/tools/llubi/loadstore_be.ll
+++ b/llvm/test/tools/llubi/loadstore_be.ll
@@ -110,7 +110,7 @@ define void @main() {
   %alloc_struct_padding = alloca {i8, i32}
   store {i8, i32} zeroinitializer, ptr %alloc_struct_padding
   %load_struct_noundef = load {i8, i32}, ptr %alloc_struct_padding, !noundef !{}
-  
+
   %alloc_ptr = alloca ptr
   store ptr %alloc_ptr, ptr %alloc_ptr
   ; It should recover the provenance.
@@ -200,10 +200,13 @@ define void @main() {
 ; CHECK-NEXT:   %load_int_non_zero_padding = load i33, ptr %alloc_padding_vec, align 8 => i33 255
 ; CHECK-NEXT:   %load_vec_non_zero_padding = load <3 x i11>, ptr %alloc_padding_vec, align 8 => { i11 0, i11 0, i11 255 }
 ; CHECK-NEXT:   %alloc_struct_padding = alloca { i8, i32 }, align 8 => ptr 0x88 [alloc_struct_padding]
+; CHECK-NEXT:   store { i8, i32 } zeroinitializer, ptr %alloc_struct_padding, align 4
 ; CHECK-NEXT:   %load_struct_noundef = load { i8, i32 }, ptr %alloc_struct_padding, align 4, !noundef !0 => { i8 0, i32 0 }
-; CHECK-NEXT:   %alloc_ptr = alloca ptr, align 8 => ptr 0x88 [alloc_ptr]
-; CHECK-NEXT:   %ptr_with_provenance = load ptr, ptr %alloc_ptr, align 8 => ptr 0x88 [alloc_ptr]
+; CHECK-NEXT:   %alloc_ptr = alloca ptr, align 8 => ptr 0x90 [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 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 0x88 [nullary]
+; CHECK-NEXT:   %ptr_without_provenance = load ptr, ptr %alloc_ptr, align 8 => ptr 0x90 [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 6f6826d884adc..3caf02c3e6eaa 100644
--- a/llvm/test/tools/llubi/loadstore_le.ll
+++ b/llvm/test/tools/llubi/loadstore_le.ll
@@ -111,7 +111,7 @@ define void @main() {
   %alloc_struct_padding = alloca {i8, i32}
   store {i8, i32} zeroinitializer, ptr %alloc_struct_padding
   %load_struct_noundef = load {i8, i32}, ptr %alloc_struct_padding, !noundef !{}
-  
+
   %alloc_ptr = alloca ptr
   store ptr %alloc_ptr, ptr %alloc_ptr
   ; It should recover the provenance.
@@ -204,11 +204,11 @@ define void @main() {
 ; CHECK-NEXT:   %alloc_struct_padding = alloca { i8, i32 }, align 8 => ptr 0x88 [alloc_struct_padding]
 ; CHECK-NEXT:   store { i8, i32 } zeroinitializer, ptr %alloc_struct_padding, align 4
 ; CHECK-NEXT:   %load_struct_noundef = load { i8, i32 }, ptr %alloc_struct_padding, align 4, !noundef !0 => { i8 0, i32 0 }
-; CHECK-NEXT:   %alloc_ptr = alloca ptr, align 8 => ptr 0x88 [alloc_ptr]
+; CHECK-NEXT:   %alloc_ptr = alloca ptr, align 8 => ptr 0x90 [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 0x88 [alloc_ptr]
-; CHECK-NEXT:   %addr_bits = load i8, ptr %alloc_ptr, align 1 => i8 -120
+; 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:   store i8 %addr_bits, ptr %alloc_ptr, align 1
-; CHECK-NEXT:   %ptr_without_provenance = load ptr, ptr %alloc_ptr, align 8 => ptr 0x88 [nullary]
+; CHECK-NEXT:   %ptr_without_provenance = load ptr, ptr %alloc_ptr, align 8 => ptr 0x90 [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 7b4309e139e79..4c2759698e260 100644
--- a/llvm/test/tools/llubi/metadata.ll
+++ b/llvm/test/tools/llubi/metadata.ll
@@ -25,13 +25,11 @@ define void @main() {
   %nofpclass_load_valid = load float, ptr %alloc, !noundef !{}, !nofpclass !{i32 3}
   %nofpclass_load_invalid = load float, ptr %alloc, !nofpclass !{i32 99}
 
-  ; TODO: Test dereferenceable[_or_null] after provenance support is ready
-
   %alloc_ptr = alloca ptr
   store ptr %alloc_ptr, ptr %alloc_ptr
-  %align_nonnull_load_valid = load ptr, ptr %alloc_ptr, !nonnull !{}, !align !{i32 8}, !noundef !{}
+  %align_nonnull_load_valid = load ptr, ptr %alloc_ptr, !nonnull !{}, !align !{i32 8}, !noundef !{}, !dereferenceable !{i32 8}, !dereferenceable_or_null !{i32 8}
   store ptr null, ptr %alloc_ptr
-  %align_load_valid = load ptr, ptr %alloc_ptr, !align !{i32 8}, !noundef !{}
+  %align_load_valid = load ptr, ptr %alloc_ptr, !align !{i32 8}, !noundef !{}, !dereferenceable_or_null !{i32 8}
   %nonnull_load_invalid = load ptr, ptr %alloc_ptr, !nonnull !{}
 
   %range_call_valid = call i32 @callee(), !noundef !{}, !range !{i32 0, i32 11}
@@ -60,9 +58,9 @@ define void @main() {
 ; 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:   store ptr %alloc_ptr, ptr %alloc_ptr, align 8
-; CHECK-NEXT:   %align_nonnull_load_valid = load ptr, ptr %alloc_ptr, align 8, !nonnull !1, !align !6, !noundef !1 => ptr 0x40 [dangling]
+; 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:   store ptr null, ptr %alloc_ptr, align 8
-; CHECK-NEXT:   %align_load_valid = load ptr, ptr %alloc_ptr, align 8, !align !6, !noundef !1 => ptr 0x0 [dangling]
+; 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
 ; CHECK-NEXT: Entering function: callee
 ; CHECK-NEXT:   ret i32 10
@@ -86,12 +84,12 @@ define void @main() {
 ; 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: Entering function: callee_ptr
-; CHECK-NEXT:   ptr %x = ptr 0x0 [dangling]
+; CHECK-NEXT:   ptr %x = ptr 0x0 [nullary]
 ; CHECK-NEXT:   ret ptr %x
 ; CHECK-NEXT: Exiting function: callee_ptr
-; CHECK-NEXT:   %align_call_invalid = call ptr @callee_ptr(ptr null), !align !6, !noundef !1 => ptr 0x0 [dangling]
+; CHECK-NEXT:   %align_call_invalid = call ptr @callee_ptr(ptr null), !align !6, !noundef !1 => ptr 0x0 [nullary]
 ; CHECK-NEXT: Entering function: callee_ptr
-; CHECK-NEXT:   ptr %x = ptr 0x0 [dangling]
+; CHECK-NEXT:   ptr %x = ptr 0x0 [nullary]
 ; CHECK-NEXT:   ret ptr %x
 ; CHECK-NEXT: Exiting function: callee_ptr
 ; CHECK-NEXT:   %nonnull_call_invalid = call ptr @callee_ptr(ptr null), !nonnull !1 => poison
@@ -106,9 +104,9 @@ define void @main() {
 ; 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: Entering function: callee_ptr
-; CHECK-NEXT:   ptr %x = ptr 0x0 [dangling]
+; CHECK-NEXT:   ptr %x = ptr 0x0 [nullary]
 ; CHECK-NEXT:   ret ptr %x
 ; CHECK-NEXT: Exiting function: callee_ptr
-; CHECK-NEXT:   %dereferenceable_or_null_call_valid2 = call ptr @callee_ptr(ptr null), !dereferenceable_or_null !6 => ptr 0x0 [dangling]
+; CHECK-NEXT:   %dereferenceable_or_null_call_valid2 = call ptr @callee_ptr(ptr null), !dereferenceable_or_null !6 => ptr 0x0 [nullary]
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: main

>From 834527593b85f7121c6fde57d7d86502335846ae Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhigec_cpp at outlook.com>
Date: Tue, 5 May 2026 16:21:12 +0800
Subject: [PATCH 5/6] [llubi] Experimental noalias support

---
 llvm/test/tools/llubi/noalias_after_return.ll |  35 ++
 .../tools/llubi/noalias_aliasing_reads.ll     |  46 ++
 .../llubi/noalias_disabled_by_default.ll      |  29 ++
 .../tools/llubi/noalias_disjoint_intervals.ll |  49 ++
 .../tools/llubi/noalias_foreign_access.ll     |  32 ++
 .../test/tools/llubi/noalias_outside_range.ll |  56 ++
 .../tools/llubi/noalias_overlapping_ranges.ll |  46 ++
 .../tools/llubi/noalias_prune_stale_node.ll   |  51 ++
 .../llubi/noalias_reserved_foreign_read.ll    |  35 ++
 llvm/tools/llubi/lib/Context.cpp              | 484 +++++++++++++++++-
 llvm/tools/llubi/lib/Context.h                | 102 ++++
 llvm/tools/llubi/lib/ExecutorBase.cpp         |  54 +-
 llvm/tools/llubi/lib/ExecutorBase.h           |   9 +-
 llvm/tools/llubi/lib/Interpreter.cpp          |  16 +-
 llvm/tools/llubi/lib/Value.h                  |  52 +-
 llvm/tools/llubi/llubi.cpp                    |  11 +
 16 files changed, 1078 insertions(+), 29 deletions(-)
 create mode 100644 llvm/test/tools/llubi/noalias_after_return.ll
 create mode 100644 llvm/test/tools/llubi/noalias_aliasing_reads.ll
 create mode 100644 llvm/test/tools/llubi/noalias_disabled_by_default.ll
 create mode 100644 llvm/test/tools/llubi/noalias_disjoint_intervals.ll
 create mode 100644 llvm/test/tools/llubi/noalias_foreign_access.ll
 create mode 100644 llvm/test/tools/llubi/noalias_outside_range.ll
 create mode 100644 llvm/test/tools/llubi/noalias_overlapping_ranges.ll
 create mode 100644 llvm/test/tools/llubi/noalias_prune_stale_node.ll
 create mode 100644 llvm/test/tools/llubi/noalias_reserved_foreign_read.ll

diff --git a/llvm/test/tools/llubi/noalias_after_return.ll b/llvm/test/tools/llubi/noalias_after_return.ll
new file mode 100644
index 0000000000000..6738a951b53c6
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_after_return.ll
@@ -0,0 +1,35 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --experimental-noalias --verbose < %s 2>&1 | FileCheck %s
+
+define void @write_one(ptr noalias %x) {
+  store i32 1, ptr %x
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  call void @write_one(ptr %a)
+  %v = load i32, ptr %a
+  store i32 2, ptr %a
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: Entering function: write_one
+; CHECK-NEXT:   ptr %x = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' based on raw/root
+; CHECK-NEXT: NoAlias: node #1 local write through node #1 on 'a' bytes [0, 4): Reserved -> Unique
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias protector
+; CHECK-NEXT:   store i32 1, ptr %x, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: protector end for node #1 triggers synthetic write on 'a' bytes [0, 4)
+; CHECK-NEXT: NoAlias: protector end: node #1 local write through node #1 on 'a' bytes [0, 4): Unique -> Unique
+; CHECK-NEXT: NoAlias: ended protector node #1
+; CHECK-NEXT: NoAlias: erased inactive protector node #1
+; CHECK-NEXT: Exiting function: write_one
+; CHECK-NEXT:   call void @write_one(ptr %a)
+; CHECK-NEXT:   %v = load i32, ptr %a, align 4 => i32 1
+; CHECK-NEXT:   store i32 2, ptr %a, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_aliasing_reads.ll b/llvm/test/tools/llubi/noalias_aliasing_reads.ll
new file mode 100644
index 0000000000000..c1fcd813c0211
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_aliasing_reads.ll
@@ -0,0 +1,46 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --experimental-noalias --verbose < %s 2>&1 | FileCheck %s
+
+define void @read_both(ptr noalias %x, ptr noalias %y) {
+  %vx = load i32, ptr %x
+  %vy = load i32, ptr %y
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  store i32 42, ptr %a
+  call void @read_both(ptr %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT:   store i32 42, ptr %a, align 4
+; CHECK-NEXT: Entering function: read_both
+; CHECK-NEXT:   ptr %x = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %y = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' based on raw/root
+; CHECK-NEXT: NoAlias: node #1 local read through node #1 on 'a' bytes [0, 4): Reserved -> ReservedL
+; CHECK-NEXT: NoAlias: node #2 foreign read through node #1 on 'a' bytes [0, 4): Reserved -> ReservedF
+; CHECK-NEXT: NoAlias: read through node #1 on 'a' bytes [0, 4) checked 2 active noalias protectors
+; CHECK-NEXT:   %vx = load i32, ptr %x, align 4 => i32 42
+; CHECK-NEXT: NoAlias: node #1 foreign read through node #2 on 'a' bytes [0, 4): ReservedL -> ReservedLF
+; CHECK-NEXT: NoAlias: node #2 local read through node #2 on 'a' bytes [0, 4): ReservedF -> ReservedLF
+; CHECK-NEXT: NoAlias: read through node #2 on 'a' bytes [0, 4) checked 2 active noalias protectors
+; CHECK-NEXT:   %vy = load i32, ptr %y, align 4 => i32 42
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: protector end for node #1 triggers synthetic read on 'a' bytes [0, 4)
+; CHECK-NEXT: NoAlias: protector end: node #1 local read through node #1 on 'a' bytes [0, 4): ReservedLF -> ReservedLF
+; CHECK-NEXT: NoAlias: protector end: node #2 foreign read through node #1 on 'a' bytes [0, 4): ReservedLF -> ReservedLF
+; CHECK-NEXT: NoAlias: ended protector node #1
+; CHECK-NEXT: NoAlias: erased inactive protector node #1
+; CHECK-NEXT: NoAlias: protector end for node #2 triggers synthetic read on 'a' bytes [0, 4)
+; CHECK-NEXT: NoAlias: protector end: node #2 local read through node #2 on 'a' bytes [0, 4): ReservedLF -> ReservedLF
+; CHECK-NEXT: NoAlias: ended protector node #2
+; CHECK-NEXT: NoAlias: erased inactive protector node #2
+; CHECK-NEXT: Exiting function: read_both
+; CHECK-NEXT:   call void @read_both(ptr %a, ptr %a)
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_disabled_by_default.ll b/llvm/test/tools/llubi/noalias_disabled_by_default.ll
new file mode 100644
index 0000000000000..65ce89ac44636
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_disabled_by_default.ll
@@ -0,0 +1,29 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --verbose < %s 2>&1 | FileCheck %s
+
+define void @violates_if_enabled(ptr noalias %x, ptr %y) {
+  store i32 1, ptr %x
+  %v = load i32, ptr %y
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  store i32 0, ptr %a
+  call void @violates_if_enabled(ptr %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT:   store i32 0, ptr %a, align 4
+; CHECK-NEXT: Entering function: violates_if_enabled
+; CHECK-NEXT:   ptr %x = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %y = ptr 0x8 [a]
+; CHECK-NEXT:   store i32 1, ptr %x, align 4
+; CHECK-NEXT:   %v = load i32, ptr %y, align 4 => i32 1
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: violates_if_enabled
+; CHECK-NEXT:   call void @violates_if_enabled(ptr %a, ptr %a)
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_disjoint_intervals.ll b/llvm/test/tools/llubi/noalias_disjoint_intervals.ll
new file mode 100644
index 0000000000000..dd9030e9ea10f
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_disjoint_intervals.ll
@@ -0,0 +1,49 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --experimental-noalias --verbose < %s 2>&1 | FileCheck %s
+
+define void @write_disjoint(ptr noalias %x, ptr noalias %y) {
+  store i32 1, ptr %x
+  store i32 2, ptr %y
+  ret void
+}
+
+define void @main() {
+  %a = alloca [2 x i32]
+  %x = getelementptr [2 x i32], ptr %a, i64 0, i64 0
+  %y = getelementptr [2 x i32], ptr %a, i64 0, i64 1
+  call void @write_disjoint(ptr %x, ptr %y)
+  %v = load [2 x i32], ptr %a
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca [2 x i32], align 4 => ptr 0x8 [a]
+; CHECK-NEXT:   %x = getelementptr [2 x i32], ptr %a, i64 0, i64 0 => ptr 0x8 [a]
+; CHECK-NEXT:   %y = getelementptr [2 x i32], ptr %a, i64 0, i64 1 => ptr 0xC [a + 4]
+; CHECK-NEXT: Entering function: write_disjoint
+; CHECK-NEXT:   ptr %x = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %y = ptr 0xC [a + 4]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' based on raw/root
+; CHECK-NEXT: NoAlias: node #1 local write through node #1 on 'a' bytes [0, 4): Reserved -> Unique
+; CHECK-NEXT: NoAlias: node #2 foreign write through node #1 on 'a' bytes [0, 4): Reserved -> Disabled
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 2 active noalias protectors
+; CHECK-NEXT:   store i32 1, ptr %x, align 4
+; CHECK-NEXT: NoAlias: node #1 foreign write through node #2 on 'a' bytes [4, 8): Reserved -> Disabled
+; CHECK-NEXT: NoAlias: node #2 local write through node #2 on 'a' bytes [4, 8): Reserved -> Unique
+; CHECK-NEXT: NoAlias: write through node #2 on 'a' bytes [4, 8) checked 2 active noalias protectors
+; CHECK-NEXT:   store i32 2, ptr %y, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: protector end for node #1 triggers synthetic write on 'a' bytes [0, 4)
+; CHECK-NEXT: NoAlias: protector end: node #1 local write through node #1 on 'a' bytes [0, 4): Unique -> Unique
+; CHECK-NEXT: NoAlias: protector end: node #2 foreign write through node #1 on 'a' bytes [0, 4): Disabled -> Disabled
+; CHECK-NEXT: NoAlias: ended protector node #1
+; CHECK-NEXT: NoAlias: erased inactive protector node #1
+; CHECK-NEXT: NoAlias: protector end for node #2 triggers synthetic write on 'a' bytes [4, 8)
+; CHECK-NEXT: NoAlias: protector end: node #2 local write through node #2 on 'a' bytes [4, 8): Unique -> Unique
+; CHECK-NEXT: NoAlias: ended protector node #2
+; CHECK-NEXT: NoAlias: erased inactive protector node #2
+; CHECK-NEXT: Exiting function: write_disjoint
+; CHECK-NEXT:   call void @write_disjoint(ptr %x, ptr %y)
+; CHECK-NEXT:   %v = load [2 x i32], ptr %a, align 4 => { i32 1, i32 2 }
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_foreign_access.ll b/llvm/test/tools/llubi/noalias_foreign_access.ll
new file mode 100644
index 0000000000000..b76effb09d746
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_foreign_access.ll
@@ -0,0 +1,32 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --experimental-noalias --verbose < %s 2>&1 | FileCheck %s
+
+define void @violates(ptr noalias %x, ptr %y) {
+  store i32 1, ptr %x
+  %v = load i32, ptr %y
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  store i32 0, ptr %a
+  call void @violates(ptr %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT:   store i32 0, ptr %a, align 4
+; CHECK-NEXT: Entering function: violates
+; CHECK-NEXT:   ptr %x = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %y = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' based on raw/root
+; CHECK-NEXT: NoAlias: node #1 local write through node #1 on 'a' bytes [0, 4): Reserved -> Unique
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias protector
+; CHECK-NEXT:   store i32 1, ptr %x, align 4
+; CHECK-NEXT: NoAlias: noalias violation: read through raw/root on 'a' bytes [0, 4) is foreign to protected node #1, but that protector is in Unique state
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %v = load i32, ptr %y, align 4 at @violates
+; CHECK-NEXT: #1   call void @violates(ptr %a, ptr %a) at @main
+; CHECK-NEXT: Immediate UB detected: noalias violation: read through raw/root on 'a' bytes [0, 4) is foreign to protected node #1, but that protector is in Unique state
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_outside_range.ll b/llvm/test/tools/llubi/noalias_outside_range.ll
new file mode 100644
index 0000000000000..aa946d0f11e84
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_outside_range.ll
@@ -0,0 +1,56 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; Ported from Miri's tests/fail/tree_borrows/outside-range.rs:
+; protector checks are byte-range precise. Foreign writes outside the bytes
+; previously used by the protected pointer do not fail, but invalidating an
+; accessed byte does.
+; RUN: not llubi --experimental-noalias --verbose < %s 2>&1 | FileCheck %s
+
+define void @stuff(ptr noalias %x, ptr %raw) {
+  %raw1 = getelementptr i8, ptr %raw, i64 1
+  store i8 42, ptr %raw1
+  %x2 = getelementptr i8, ptr %x, i64 2
+  %v2 = load i8, ptr %x2
+  %x3 = getelementptr i8, ptr %x, i64 3
+  %v3 = load i8, ptr %x3
+  %raw3 = getelementptr i8, ptr %raw, i64 3
+  store i8 42, ptr %raw3
+  ret void
+}
+
+define void @main() {
+  %data = alloca [4 x i8]
+  %raw = getelementptr [4 x i8], ptr %data, i64 0, i64 0
+  call void @stuff(ptr %raw, ptr %raw)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %data = alloca [4 x i8], align 1 => ptr 0x8 [data]
+; CHECK-NEXT:   %raw = getelementptr [4 x i8], ptr %data, i64 0, i64 0 => ptr 0x8 [data]
+; CHECK-NEXT: Entering function: stuff
+; CHECK-NEXT:   ptr %x = ptr 0x8 [data]
+; CHECK-NEXT:   ptr %raw = ptr 0x8 [data]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'data' based on raw/root
+; CHECK-NEXT:   %raw1 = getelementptr i8, ptr %raw, i64 1 => ptr 0x9 [data + 1]
+; CHECK-NEXT: NoAlias: node #1 foreign write through raw/root on 'data' bytes [1, 2): Reserved -> Disabled
+; CHECK-NEXT: NoAlias: write through raw/root on 'data' bytes [1, 2) checked 1 active noalias protector
+; CHECK-NEXT:   store i8 42, ptr %raw1, align 1
+; CHECK-NEXT:   %x2 = getelementptr i8, ptr %x, i64 2 => ptr 0xA [data + 2]
+; CHECK-NEXT: NoAlias: node #1 foreign read through raw/root on 'data' bytes [2, 3): Reserved -> ReservedF
+; CHECK-NEXT: NoAlias: read through raw/root on 'data' bytes [2, 3) checked 1 active noalias protector
+; CHECK-NEXT:   %v2 = load i8, ptr %x2, align 1 => i8 62
+; CHECK-NEXT:   %x3 = getelementptr i8, ptr %x, i64 3 => ptr 0xB [data + 3]
+; CHECK-NEXT: NoAlias: node #1 foreign read through raw/root on 'data' bytes [3, 4): Reserved -> ReservedF
+; CHECK-NEXT: NoAlias: read through raw/root on 'data' bytes [3, 4) checked 1 active noalias protector
+; CHECK-NEXT:   %v3 = load i8, ptr %x3, align 1 => i8 -117
+; CHECK-NEXT:   %raw3 = getelementptr i8, ptr %raw, i64 3 => ptr 0xB [data + 3]
+; CHECK-NEXT: NoAlias: node #1 foreign write through raw/root on 'data' bytes [3, 4): ReservedF -> Disabled
+; CHECK-NEXT: NoAlias: write through raw/root on 'data' bytes [3, 4) checked 1 active noalias protector
+; CHECK-NEXT:   store i8 42, ptr %raw3, align 1
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended protector node #1
+; CHECK-NEXT: NoAlias: erased inactive protector node #1
+; CHECK-NEXT: Exiting function: stuff
+; CHECK-NEXT:   call void @stuff(ptr %raw, ptr %raw)
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_overlapping_ranges.ll b/llvm/test/tools/llubi/noalias_overlapping_ranges.ll
new file mode 100644
index 0000000000000..068e4a9136ee9
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_overlapping_ranges.ll
@@ -0,0 +1,46 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; Ported from Miri's tests/fail/both_borrows/buggy_split_at_mut.rs:
+; two supposedly independent mutable ranges overlap, so writes through both
+; protected noalias arguments are rejected.
+; RUN: not llubi --experimental-noalias --verbose < %s 2>&1 | FileCheck %s
+
+define void @write_overlap(ptr noalias %a, ptr noalias %b) {
+  %a1 = getelementptr [4 x i32], ptr %a, i64 0, i64 1
+  store i32 5, ptr %a1
+  %b1 = getelementptr [4 x i32], ptr %b, i64 0, i64 1
+  store i32 6, ptr %b1
+  ret void
+}
+
+define void @main() {
+  %array = alloca [4 x i32]
+  call void @write_overlap(ptr %array, ptr %array)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %array = alloca [4 x i32], align 4 => ptr 0x8 [array]
+; CHECK-NEXT: Entering function: write_overlap
+; CHECK-NEXT:   ptr %a = ptr 0x8 [array]
+; CHECK-NEXT:   ptr %b = ptr 0x8 [array]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'array' based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #2 for 'array' based on raw/root
+; CHECK-NEXT:   %a1 = getelementptr [4 x i32], ptr %a, i64 0, i64 1 => ptr 0xC [array + 4]
+; CHECK-NEXT: NoAlias: node #1 foreign write through raw/root on 'array' bytes [4, 8): Reserved -> Disabled
+; CHECK-NEXT: NoAlias: node #2 foreign write through raw/root on 'array' bytes [4, 8): Reserved -> Disabled
+; CHECK-NEXT: NoAlias: write through raw/root on 'array' bytes [4, 8) checked 2 active noalias protectors
+; CHECK-NEXT:   store i32 5, ptr %a1, align 4
+; CHECK-NEXT:   %b1 = getelementptr [4 x i32], ptr %b, i64 0, i64 1 => ptr 0xC [array + 4]
+; CHECK-NEXT: NoAlias: node #1 foreign write through raw/root on 'array' bytes [4, 8): Disabled -> Disabled
+; CHECK-NEXT: NoAlias: node #2 foreign write through raw/root on 'array' bytes [4, 8): Disabled -> Disabled
+; CHECK-NEXT: NoAlias: write through raw/root on 'array' bytes [4, 8) checked 2 active noalias protectors
+; CHECK-NEXT:   store i32 6, ptr %b1, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended protector node #1
+; CHECK-NEXT: NoAlias: erased inactive protector node #1
+; CHECK-NEXT: NoAlias: ended protector node #2
+; CHECK-NEXT: NoAlias: erased inactive protector node #2
+; CHECK-NEXT: Exiting function: write_overlap
+; CHECK-NEXT:   call void @write_overlap(ptr %array, ptr %array)
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_prune_stale_node.ll b/llvm/test/tools/llubi/noalias_prune_stale_node.ll
new file mode 100644
index 0000000000000..da83ef7d01659
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_prune_stale_node.ll
@@ -0,0 +1,51 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --experimental-noalias --verbose < %s 2>&1 | FileCheck %s
+
+define void @capture(ptr noalias %x, ptr %slot) {
+  store ptr %x, ptr %slot
+  ret void
+}
+
+define void @write_one(ptr noalias %x) {
+  store i32 1, ptr %x
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  %slot = alloca ptr
+  call void @capture(ptr %a, ptr %slot)
+  %stale = load ptr, ptr %slot
+  call void @write_one(ptr %stale)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT:   %slot = alloca ptr, align 8 => ptr 0x10 [slot]
+; CHECK-NEXT: Entering function: capture
+; CHECK-NEXT:   ptr %x = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %slot = ptr 0x10 [slot]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' based on raw/root
+; CHECK-NEXT:   store ptr %x, ptr %slot, align 8
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended protector node #1
+; CHECK-NEXT: NoAlias: erased inactive protector node #1
+; CHECK-NEXT: Exiting function: capture
+; CHECK-NEXT:   call void @capture(ptr %a, ptr %slot)
+; CHECK-NEXT:   %stale = load ptr, ptr %slot, align 8 => ptr 0x8 [a]
+; CHECK-NEXT: Entering function: write_one
+; CHECK-NEXT:   ptr %x = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' based on raw/root
+; CHECK-NEXT: NoAlias: node #2 local write through node #2 on 'a' bytes [0, 4): Reserved -> Unique
+; CHECK-NEXT: NoAlias: write through node #2 on 'a' bytes [0, 4) checked 1 active noalias protector
+; CHECK-NEXT:   store i32 1, ptr %x, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: protector end for node #2 triggers synthetic write on 'a' bytes [0, 4)
+; CHECK-NEXT: NoAlias: protector end: node #2 local write through node #2 on 'a' bytes [0, 4): Unique -> Unique
+; CHECK-NEXT: NoAlias: ended protector node #2
+; CHECK-NEXT: NoAlias: erased inactive protector node #2
+; CHECK-NEXT: Exiting function: write_one
+; CHECK-NEXT:   call void @write_one(ptr %stale)
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_reserved_foreign_read.ll b/llvm/test/tools/llubi/noalias_reserved_foreign_read.ll
new file mode 100644
index 0000000000000..671659b1759f7
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_reserved_foreign_read.ll
@@ -0,0 +1,35 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; Adapted from Miri's tests/pass/tree_borrows/reserved.rs int_protected_read:
+; a protected Reserved pointer can observe a foreign read, but a later local
+; write is then forbidden.
+; RUN: not llubi --experimental-noalias --verbose < %s 2>&1 | FileCheck %s
+
+define void @foreign_read_then_local_write(ptr noalias %x, ptr %raw) {
+  %v = load i32, ptr %raw
+  store i32 1, ptr %x
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  store i32 0, ptr %a
+  call void @foreign_read_then_local_write(ptr %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT:   store i32 0, ptr %a, align 4
+; CHECK-NEXT: Entering function: foreign_read_then_local_write
+; CHECK-NEXT:   ptr %x = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %raw = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' based on raw/root
+; CHECK-NEXT: NoAlias: node #1 foreign read through raw/root on 'a' bytes [0, 4): Reserved -> ReservedF
+; CHECK-NEXT: NoAlias: read through raw/root on 'a' bytes [0, 4) checked 1 active noalias protector
+; CHECK-NEXT:   %v = load i32, ptr %raw, align 4 => i32 0
+; CHECK-NEXT: NoAlias: noalias violation: write through node #1 on 'a' bytes [0, 4) is local to protected node #1, but that protector is in ReservedF state
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i32 1, ptr %x, align 4 at @foreign_read_then_local_write
+; CHECK-NEXT: #1   call void @foreign_read_then_local_write(ptr %a, ptr %a) at @main
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through node #1 on 'a' bytes [0, 4) is local to protected node #1, but that protector is in ReservedF state
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/tools/llubi/lib/Context.cpp b/llvm/tools/llubi/lib/Context.cpp
index b4a2d077e4cc1..04dbd15bfbdbc 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -12,6 +12,9 @@
 
 #include "Context.h"
 #include "llvm/Support/MathExtras.h"
+#include "llvm/Support/raw_ostream.h"
+
+#include <algorithm>
 
 namespace llvm::ubi {
 
@@ -143,6 +146,10 @@ AnyValue Context::fromBytes(ConstBytesView Bytes, Type *Ty,
   SmallVector<uint64_t> RawTagBits;
   if (Ty->isPointerTy())
     RawTagBits.resize(NumWords);
+  bool IsNoAliasValid = ExperimentalNoAlias && Ty->isPointerTy();
+  std::optional<uint64_t> LoadedNoAliasNode;
+  bool SawNoAliasBits = false;
+  bool SawMissingNoAliasBits = false;
   for (uint32_t I = 0; I < NumBitsToExtract; I += 8) {
     // Try to form a 'logical' byte that represents the bits in the range
     // [BitsStart, BitsEnd].
@@ -191,6 +198,22 @@ AnyValue Context::fromBytes(ConstBytesView Bytes, Type *Ty,
         IsTagValid = false;
       }
     }
+    if (IsNoAliasValid) {
+      uint8_t NoAliasMask = LogicalByte.NoAliasMask & Mask;
+      if (NoAliasMask == Mask) {
+        SawNoAliasBits = true;
+        if (!LoadedNoAliasNode)
+          LoadedNoAliasNode = LogicalByte.NoAliasNode;
+        else if (*LoadedNoAliasNode != LogicalByte.NoAliasNode)
+          IsNoAliasValid = false;
+      } else if (!NoAliasMask) {
+        SawMissingNoAliasBits = true;
+      } else {
+        IsNoAliasValid = false;
+      }
+      if (SawNoAliasBits && SawMissingNoAliasBits)
+        IsNoAliasValid = false;
+    }
   }
   OffsetInBits = NewOffsetInBits;
 
@@ -215,7 +238,9 @@ AnyValue Context::fromBytes(ConstBytesView Bytes, Type *Ty,
   if (IsTagValid) {
     // TODO: decode metadata bits from the tag.
     APInt Tag(NumBitsToExtract, RawTagBits);
-    return Pointer(CapturedMemoryObjects.lookup(Tag), Bits);
+    return Pointer(CapturedMemoryObjects.lookup(Tag), Bits,
+                   IsNoAliasValid && LoadedNoAliasNode ? *LoadedNoAliasNode
+                                                       : 0);
   }
   return Pointer(Bits);
 }
@@ -296,7 +321,8 @@ void Context::toBytes(const AnyValue &Val, Type *Ty, uint32_t OffsetInBits,
   if (PaddingBits)
     NewOffsetInBits = alignTo(NewOffsetInBits, 8);
   bool NeedsPadding = NewOffsetInBits != OffsetInBits + NumBits;
-  auto WriteBits = [&](const APInt &Bits, const APInt *TagBits) {
+  auto WriteBits = [&](const APInt &Bits, const APInt *TagBits,
+                       uint64_t NoAliasNode) {
     for (uint32_t I = 0, E = Bits.getBitWidth(); I < E; I += 8) {
       uint32_t NumBitsInByte = std::min(8U, E - I);
       uint32_t BitsStart = OffsetInBits + I;
@@ -328,6 +354,15 @@ void Context::toBytes(const AnyValue &Val, Type *Ty, uint32_t OffsetInBits,
               static_cast<uint8_t>((1U << (BitsEnd % 8 + 1)) - 1),
               static_cast<uint8_t>(TagBitsVal >> (8 - (BitsStart % 8))));
       }
+      if (NoAliasNode) {
+        Bytes[BitsStart / 8].writeNoAliasBits(
+            static_cast<uint8_t>(((1U << NumBitsInByte) - 1)
+                                 << (BitsStart % 8)),
+            NoAliasNode);
+        if (((BitsStart ^ BitsEnd) & ~7) != 0)
+          Bytes[BitsEnd / 8].writeNoAliasBits(
+              static_cast<uint8_t>((1U << (BitsEnd % 8 + 1)) - 1), NoAliasNode);
+      }
     }
   };
   if (Val.isPoison()) {
@@ -342,24 +377,26 @@ void Context::toBytes(const AnyValue &Val, Type *Ty, uint32_t OffsetInBits,
   } else if (Ty->isIntegerTy()) {
     auto &Bits = Val.asInteger();
     WriteBits(NeedsPadding ? Bits.zext(NewOffsetInBits - OffsetInBits) : Bits,
-              /*TagBits=*/nullptr);
+              /*TagBits=*/nullptr, /*NoAliasNode=*/0);
   } else if (Ty->isFloatingPointTy()) {
     auto Bits = Val.asFloat().bitcastToAPInt();
     WriteBits(NeedsPadding ? Bits.zext(NewOffsetInBits - OffsetInBits) : Bits,
-              /*TagBits=*/nullptr);
+              /*TagBits=*/nullptr, /*NoAliasNode=*/0);
   } else if (Ty->isPointerTy()) {
     auto &AddressBits = Val.asPointer().address();
+    uint64_t NoAliasNode =
+        ExperimentalNoAlias ? Val.asPointer().getNoAliasNodeID() : 0;
     if (auto *MO = Val.asPointer().getMemoryObject()) {
       APInt Tag = getTag(AddressBits.getBitWidth(), MO);
       if (NeedsPadding)
         Tag = Tag.zext(NewOffsetInBits - OffsetInBits);
       WriteBits(NeedsPadding ? AddressBits.zext(NewOffsetInBits - OffsetInBits)
                              : AddressBits,
-                &Tag);
+                &Tag, NoAliasNode);
     } else {
       WriteBits(NeedsPadding ? AddressBits.zext(NewOffsetInBits - OffsetInBits)
                              : AddressBits,
-                /*TagBits=*/nullptr);
+                /*TagBits=*/nullptr, NoAliasNode);
     }
   } else {
     llvm_unreachable("Unsupported scalar type.");
@@ -548,6 +585,7 @@ bool Context::free(const MemoryObject &Obj) {
     return false;
 
   UsedMem -= std::max(It->second->getSize(), static_cast<uint64_t>(1));
+  clearNoAliasState(*It->second);
   It->second->markAsFreed();
   MemoryObjects.erase(It);
   return true;
@@ -613,4 +651,438 @@ bool MemoryObject::isHeapAllocated() const {
   llvm_unreachable("Unknown MemAllocKind");
 }
 
+bool Context::isNoAliasAncestor(uint64_t Ancestor, uint64_t Descendant) const {
+  if (!Ancestor || !Descendant)
+    return false;
+  // Parent links are stable while a descendant is active. If a stale node id
+  // was pruned, reaching a missing node means the relationship no longer
+  // exists.
+  for (uint64_t NodeID = Descendant; NodeID;) {
+    if (NodeID == Ancestor)
+      return true;
+    const auto It = NoAliasNodes.find(NodeID);
+    if (It == NoAliasNodes.end())
+      return false;
+    NodeID = It->second.Parent;
+  }
+  return false;
+}
+
+bool Context::hasActiveNoAliasDescendant(uint64_t NodeID) const {
+  for (const auto &[CandidateID, Candidate] : NoAliasNodes) {
+    if (!Candidate.Active || CandidateID == NodeID)
+      continue;
+    if (isNoAliasAncestor(NodeID, CandidateID))
+      return true;
+  }
+  return false;
+}
+
+void Context::tryEraseInactiveNoAliasNode(uint64_t NodeID) {
+  const auto It = NoAliasNodes.find(NodeID);
+  if (It == NoAliasNodes.end() || It->second.Active)
+    return;
+  if (hasActiveNoAliasDescendant(NodeID))
+    return;
+
+  // An inactive node can still be relevant as the parent of a live child. Once
+  // that is no longer true, stale pointers carrying this ID should behave like
+  // raw/root pointers during future retagging.
+  const uint64_t Parent = It->second.Parent;
+  appendNoAliasEvent("erased inactive protector " + getNoAliasNodeName(NodeID));
+  NoAliasNodes.erase(It);
+  if (Parent)
+    tryEraseInactiveNoAliasNode(Parent);
+}
+
+StringRef Context::getNoAliasAccessKindName(NoAliasAccessKind Kind) {
+  switch (Kind) {
+  case NoAliasAccessKind::Read:
+    return "read";
+  case NoAliasAccessKind::Write:
+    return "write";
+  }
+  llvm_unreachable("Unknown NoAliasAccessKind");
+}
+
+StringRef Context::getNoAliasStateName(NoAliasState State) {
+  switch (State) {
+  case NoAliasState::Reserved:
+    return "Reserved";
+  case NoAliasState::ReservedL:
+    return "ReservedL";
+  case NoAliasState::ReservedF:
+    return "ReservedF";
+  case NoAliasState::ReservedLF:
+    return "ReservedLF";
+  case NoAliasState::Unique:
+    return "Unique";
+  case NoAliasState::Disabled:
+    return "Disabled";
+  case NoAliasState::Dummy:
+    return "Dummy";
+  }
+  llvm_unreachable("Unknown NoAliasState");
+}
+
+std::string Context::getNoAliasNodeName(uint64_t NodeID) {
+  if (!NodeID)
+    return "raw/root";
+  std::string S;
+  raw_string_ostream OS(S);
+  OS << "node #" << NodeID;
+  return S;
+}
+
+std::string Context::getNoAliasObjectName(const MemoryObject &MO) {
+  if (MO.getName().empty()) {
+    std::string S;
+    raw_string_ostream OS(S);
+    OS << "object at 0x";
+    OS.write_hex(MO.getAddress());
+    return S;
+  }
+  return ("'" + MO.getName() + "'").str();
+}
+
+void Context::appendNoAliasEvent(std::string Msg) {
+  NoAliasEvents.push_back(std::move(Msg));
+}
+
+void Context::setNoAliasViolation(uint64_t ProtectedNodeId, uint64_t AccessNode,
+                                  const MemoryObject &MO, uint64_t Begin,
+                                  uint64_t End, NoAliasAccessKind Kind,
+                                  bool IsLocal, NoAliasState State,
+                                  bool IsProtectorEndAction) {
+  std::string S;
+  raw_string_ostream OS(S);
+  OS << "noalias ";
+  if (IsProtectorEndAction)
+    OS << "protector end ";
+  OS << "violation: " << getNoAliasAccessKindName(Kind) << " through "
+     << getNoAliasNodeName(AccessNode) << " on " << getNoAliasObjectName(MO)
+     << " bytes [" << Begin << ", " << End << ") is "
+     << (IsLocal ? "local" : "foreign") << " to protected "
+     << getNoAliasNodeName(ProtectedNodeId) << ", but that protector is in "
+     << getNoAliasStateName(State) << " state";
+  LastNoAliasError = std::move(S);
+  appendNoAliasEvent(LastNoAliasError);
+}
+
+std::optional<Context::NoAliasState>
+Context::transitionNoAliasState(NoAliasState State, NoAliasAccessKind Kind,
+                                bool IsLocal) {
+  if (State == NoAliasState::Dummy)
+    return NoAliasState::Dummy;
+
+  // For the complete state machine, please refer to the original blog
+  // https://jhostert.de/blog/2025/noalias/.
+  const bool IsWrite = Kind == NoAliasAccessKind::Write;
+  switch (State) {
+  case NoAliasState::Reserved: {
+    if (!IsWrite)
+      return IsLocal ? NoAliasState::ReservedL : NoAliasState::ReservedF;
+    return IsLocal ? NoAliasState::Unique : NoAliasState::Disabled;
+  }
+  case NoAliasState::ReservedL: {
+    if (IsLocal)
+      return IsWrite ? NoAliasState::Unique : NoAliasState::ReservedL;
+    if (!IsWrite)
+      return NoAliasState::ReservedLF;
+    return std::nullopt;
+  }
+  case NoAliasState::ReservedF: {
+    if (!IsLocal)
+      return IsWrite ? NoAliasState::Disabled : NoAliasState::ReservedF;
+    if (!IsWrite)
+      return NoAliasState::ReservedLF;
+    return std::nullopt;
+  }
+  case NoAliasState::ReservedLF: {
+    if (!IsWrite)
+      return NoAliasState::ReservedLF;
+    return std::nullopt;
+  }
+  case NoAliasState::Unique: {
+    if (IsLocal)
+      return NoAliasState::Unique;
+    return std::nullopt;
+  }
+  case NoAliasState::Disabled: {
+    if (!IsLocal)
+      return NoAliasState::Disabled;
+    return std::nullopt;
+  }
+  case NoAliasState::Dummy:
+    llvm_unreachable("Dummy state should be handled earlier");
+  }
+  llvm_unreachable("Unknown NoAliasState");
+}
+
+bool Context::accessNoAliasImpl(MemoryObject &MO, uint64_t Offset,
+                                uint64_t Size, uint64_t AccessNode,
+                                NoAliasAccessKind Kind,
+                                uint64_t SkipDescendantsOf) {
+  if (!ActiveNoAliasScopes || !Size)
+    return true;
+
+  const auto It = NoAliasNodesByObject.find(&MO);
+  if (It == NoAliasNodesByObject.end())
+    return true;
+
+  const uint64_t End = Offset + Size;
+  uint32_t CheckedNodes = 0;
+  for (uint64_t NodeID : It->second) {
+    auto NodeIt = NoAliasNodes.find(NodeID);
+    if (NodeIt == NoAliasNodes.end() || !NodeIt->second.Active)
+      continue;
+    if (SkipDescendantsOf && NodeID != SkipDescendantsOf &&
+        isNoAliasAncestor(SkipDescendantsOf, NodeID))
+      continue;
+    ++CheckedNodes;
+    // A protected node judges the same concrete memory access differently
+    // depending on whether the pointer used for the access is in its subtree.
+    if (const bool IsLocal = isNoAliasAncestor(NodeID, AccessNode);
+        !updateNoAliasNodeForAccess(
+            NodeIt->second, Offset, End, Kind, IsLocal, NodeID, AccessNode,
+            /*IsProtectorEndAction=*/SkipDescendantsOf != 0))
+      return false;
+  }
+  if (CheckedNodes && !SkipDescendantsOf) {
+    std::string S;
+    raw_string_ostream OS(S);
+    OS << getNoAliasAccessKindName(Kind) << " through "
+       << getNoAliasNodeName(AccessNode) << " on " << getNoAliasObjectName(MO)
+       << " bytes [" << Offset << ", " << End << ") checked " << CheckedNodes
+       << " active noalias protector" << (CheckedNodes == 1 ? "" : "s");
+    appendNoAliasEvent(std::move(S));
+  }
+  return true;
+}
+
+bool Context::updateNoAliasNodeForAccess(NoAliasNode &Node, uint64_t Begin,
+                                         uint64_t End, NoAliasAccessKind Kind,
+                                         bool IsLocal, uint64_t ProtectedNodeID,
+                                         uint64_t AccessNode,
+                                         bool IsProtectorEndAction) {
+  assert(Begin < End && "empty accesses should not reach noalias tracking");
+
+  SmallVector<NoAliasStateRun, 4> NewRuns;
+  auto AppendRun = [&](uint64_t RunBegin, uint64_t RunEnd, NoAliasState State) {
+    if (RunBegin == RunEnd || State == NoAliasState::Reserved)
+      return;
+    if (!NewRuns.empty() && NewRuns.back().End == RunBegin &&
+        NewRuns.back().State == State) {
+      NewRuns.back().End = RunEnd;
+      return;
+    }
+    NewRuns.push_back({RunBegin, RunEnd, State});
+  };
+
+  auto AppendTransitioned = [&](uint64_t RunBegin, uint64_t RunEnd,
+                                NoAliasState State) -> bool {
+    if (RunBegin == RunEnd)
+      return true;
+    std::optional<NoAliasState> NewState =
+        transitionNoAliasState(State, Kind, IsLocal);
+    if (!NewState) {
+      setNoAliasViolation(ProtectedNodeID, AccessNode, *Node.Object, RunBegin,
+                          RunEnd, Kind, IsLocal, State, IsProtectorEndAction);
+      return false;
+    }
+    std::string S;
+    raw_string_ostream OS(S);
+    if (IsProtectorEndAction)
+      OS << "protector end: ";
+    OS << getNoAliasNodeName(ProtectedNodeID) << ' '
+       << (IsLocal ? "local" : "foreign") << ' '
+       << getNoAliasAccessKindName(Kind) << " through "
+       << getNoAliasNodeName(AccessNode) << " on "
+       << getNoAliasObjectName(*Node.Object) << " bytes [" << RunBegin << ", "
+       << RunEnd << "): " << getNoAliasStateName(State) << " -> "
+       << getNoAliasStateName(*NewState);
+    appendNoAliasEvent(std::move(S));
+    AppendRun(RunBegin, RunEnd, *NewState);
+    return true;
+  };
+
+  uint64_t Cur = Begin;
+  bool InsertedAccessTail = false;
+  for (const NoAliasStateRun &Run : Node.States) {
+    if (Run.End <= Begin) {
+      AppendRun(Run.Begin, Run.End, Run.State);
+      continue;
+    }
+    if (Run.Begin >= End) {
+      if (!InsertedAccessTail) {
+        // The access ends before this run begins, so [Cur, End) is an implicit
+        // Reserved gap that still needs transition.
+        if (!AppendTransitioned(Cur, End, NoAliasState::Reserved))
+          return false;
+        InsertedAccessTail = true;
+      }
+      AppendRun(Run.Begin, Run.End, Run.State);
+      continue;
+    }
+
+    if (Run.Begin < Begin)
+      AppendRun(Run.Begin, Begin, Run.State);
+
+    const uint64_t OverlapBegin = std::max(Cur, Run.Begin);
+    // Any gap between the previous covered byte and this run is also implicit
+    // Reserved state.
+    if (!AppendTransitioned(Cur, OverlapBegin, NoAliasState::Reserved))
+      return false;
+
+    const uint64_t OverlapEnd = std::min(End, Run.End);
+    if (!AppendTransitioned(OverlapBegin, OverlapEnd, Run.State))
+      return false;
+    Cur = OverlapEnd;
+
+    if (Run.End > End) {
+      AppendRun(End, Run.End, Run.State);
+      InsertedAccessTail = true;
+    }
+  }
+  if (!InsertedAccessTail) {
+    if (!AppendTransitioned(Cur, End, NoAliasState::Reserved))
+      return false;
+  }
+
+  Node.States = std::move(NewRuns);
+  return true;
+}
+
+Pointer Context::createNoAliasPointer(const Pointer &Ptr) {
+  if (!ExperimentalNoAlias)
+    return Ptr;
+
+  MemoryObject *MO = Ptr.getMemoryObject();
+  if (!MO)
+    return Ptr;
+
+  const uint64_t NodeID = NextNoAliasNode++;
+  uint64_t Parent = Ptr.getNoAliasNodeID();
+  // If the parent node was pruned after its protector ended, the incoming
+  // pointer is treated as a raw/root-derived pointer for this new scope.
+  if (Parent && NoAliasNodes.find(Parent) == NoAliasNodes.end())
+    Parent = 0;
+  NoAliasNode Node;
+  Node.Parent = Parent;
+  Node.Object = MO;
+  Node.Active = true;
+  NoAliasNodes.try_emplace(NodeID, std::move(Node));
+  NoAliasNodesByObject[MO].push_back(NodeID);
+  ++ActiveNoAliasScopes;
+  std::string S;
+  raw_string_ostream OS(S);
+  OS << "created protector " << getNoAliasNodeName(NodeID) << " for "
+     << getNoAliasObjectName(*MO) << " based on " << getNoAliasNodeName(Parent);
+  appendNoAliasEvent(std::move(S));
+  return Ptr.getWithNoAliasNode(NodeID);
+}
+
+bool Context::accessNoAlias(MemoryObject &MO, uint64_t Offset, uint64_t Size,
+                            uint64_t AccessNode, NoAliasAccessKind Kind) {
+  if (!ExperimentalNoAlias)
+    return true;
+
+  return accessNoAliasImpl(MO, Offset, Size, AccessNode, Kind,
+                           /*SkipDescendantsOf=*/0);
+}
+
+bool Context::endNoAliasScopes(ArrayRef<uint64_t> Nodes) {
+  if (!ExperimentalNoAlias)
+    return true;
+
+  for (uint64_t NodeID : Nodes) {
+    auto It = NoAliasNodes.find(NodeID);
+    if (It == NoAliasNodes.end() || !It->second.Active)
+      continue;
+
+    SmallVector<NoAliasStateRun, 4> States(It->second.States.begin(),
+                                           It->second.States.end());
+    for (const NoAliasStateRun &Run : States) {
+      std::optional<NoAliasAccessKind> EndAction;
+      // Protector end actions. Quote from the original blog:
+      // "Unique triggers writes, ReservedL and ReservedLF triggers reads, and
+      // the other states trigger nothing since they have not yet been locally
+      // accessed. Like in Tree Borrows, these end actions are “special” in
+      // that they don’t affect children of the node which was protected."
+      switch (Run.State) {
+      case NoAliasState::Unique:
+        EndAction = NoAliasAccessKind::Write;
+        break;
+      case NoAliasState::ReservedL:
+      case NoAliasState::ReservedLF:
+        EndAction = NoAliasAccessKind::Read;
+        break;
+      case NoAliasState::Reserved:
+      case NoAliasState::ReservedF:
+      case NoAliasState::Disabled:
+      case NoAliasState::Dummy:
+        break;
+      }
+      if (EndAction) {
+        std::string S;
+        raw_string_ostream OS(S);
+        OS << "protector end for " << getNoAliasNodeName(NodeID)
+           << " triggers synthetic " << getNoAliasAccessKindName(*EndAction)
+           << " on " << getNoAliasObjectName(*It->second.Object) << " bytes ["
+           << Run.Begin << ", " << Run.End << ")";
+        appendNoAliasEvent(std::move(S));
+        if (!accessNoAliasImpl(*It->second.Object, Run.Begin,
+                               Run.End - Run.Begin, NodeID, *EndAction,
+                               /*SkipDescendantsOf=*/NodeID))
+          return false;
+      }
+    }
+
+    It->second.Active = false;
+    It->second.States.clear();
+    // Remove inactive nodes from the per-object active list immediately, but
+    // keep the node record itself until no active child depends on its parent
+    // identity.
+    if (auto ObjIt = NoAliasNodesByObject.find(It->second.Object);
+        ObjIt != NoAliasNodesByObject.end()) {
+      SmallVectorImpl<uint64_t> &ObjectNodes = ObjIt->second;
+      ObjectNodes.erase(
+          std::remove(ObjectNodes.begin(), ObjectNodes.end(), NodeID),
+          ObjectNodes.end());
+      if (ObjectNodes.empty())
+        NoAliasNodesByObject.erase(ObjIt);
+    }
+    assert(ActiveNoAliasScopes && "mismatched noalias protector count");
+    --ActiveNoAliasScopes;
+    appendNoAliasEvent("ended protector " + getNoAliasNodeName(NodeID));
+    tryEraseInactiveNoAliasNode(NodeID);
+  }
+  return true;
+}
+
+SmallVector<std::string, 4> Context::takeNoAliasEvents() {
+  SmallVector<std::string, 8> Events;
+  Events.swap(NoAliasEvents);
+  return Events;
+}
+
+void Context::clearNoAliasState(const MemoryObject &MO) {
+  if (!ExperimentalNoAlias)
+    return;
+
+  const auto It = NoAliasNodesByObject.find(&MO);
+  if (It == NoAliasNodesByObject.end())
+    return;
+  for (uint64_t NodeID : It->second) {
+    auto NodeIt = NoAliasNodes.find(NodeID);
+    if (NodeIt == NoAliasNodes.end() || !NodeIt->second.Active)
+      continue;
+    NodeIt->second.Active = false;
+    NodeIt->second.States.clear();
+    assert(ActiveNoAliasScopes && "mismatched noalias protector count");
+    --ActiveNoAliasScopes;
+  }
+  NoAliasNodesByObject.erase(It);
+}
+
 } // namespace llvm::ubi
diff --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index 96b30f1382197..924d1bf8a7f79 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -14,7 +14,9 @@
 #include "llvm/Analysis/TargetLibraryInfo.h"
 #include "llvm/IR/Module.h"
 #include <map>
+#include <optional>
 #include <random>
+#include <string>
 
 namespace llvm::ubi {
 
@@ -83,6 +85,8 @@ struct ProgramExitInfo {
   }
 };
 
+enum class NoAliasAccessKind { Read, Write };
+
 class MemoryObject : public RefCountedBase<MemoryObject> {
   uint64_t Address;
   uint64_t Size;
@@ -167,6 +171,7 @@ class EventHandler {
     return true;
   }
   virtual void onProgramExit(const ProgramExitInfo &ExitInfo) {}
+  virtual bool onNoAliasEvent(StringRef Msg) { return true; }
   virtual bool onPrint(StringRef Msg) {
     outs() << Msg;
     outs().flush();
@@ -207,6 +212,7 @@ class Context {
   uint32_t MaxSteps = 0;
   uint32_t MaxStackDepth = 256;
   UndefValueBehavior UndefBehavior = UndefValueBehavior::NonDeterministic;
+  bool ExperimentalNoAlias = false;
 
   std::mt19937_64 Rng;
   APInt generateRandomAPInt(uint32_t BitWidth);
@@ -226,6 +232,48 @@ class Context {
   // TODO: Maintains a global list of 'exposed' provenances. This is used to
   // convert an address back to a pointer with a previously exposed provenance.
 
+  /// Experimental noalias states (see https://jhostert.de/blog/2025/noalias/).
+  /// The states Frozen and FrozenL from the original state machine are omitted
+  /// as proposed. Note that the Reserved state is the implicit default and is
+  /// intentionally omitted from sparse state runs below.
+  enum class NoAliasState : uint8_t {
+    Reserved,
+    ReservedL,
+    ReservedF,
+    ReservedLF,
+    Unique,
+    Disabled,
+    Dummy,
+  };
+
+  /// A non-Reserved state over the byte interval [Begin, End).
+  struct NoAliasStateRun {
+    uint64_t Begin;
+    uint64_t End;
+    NoAliasState State;
+  };
+
+  /// A protected noalias node created by retagging a noalias function argument.
+  /// Parent is another noalias node, or 0 for the raw/root parent. The
+  /// underlying pointer provenance remains represented by Pointer::Obj.
+  struct NoAliasNode {
+    uint64_t Parent = 0;
+    MemoryObject *Object = nullptr;
+    bool Active = false;
+    // Run-Length Encoding to reduce memory consumption.
+    SmallVector<NoAliasStateRun, 1> States;
+  };
+
+  // The node ID 0 is reserved for raw/root nodes.
+  uint64_t NextNoAliasNode = 1;
+  uint64_t ActiveNoAliasScopes = 0;
+  DenseMap<uint64_t, NoAliasNode> NoAliasNodes;
+  DenseMap<MemoryObject *, SmallVector<uint64_t, 2>> NoAliasNodesByObject;
+
+  // noalias-related diagnostics
+  std::string LastNoAliasError;
+  SmallVector<std::string, 4> NoAliasEvents;
+
   /// Get the tag for a pointer to the given memory object.
   /// TODO: encode metadata bits into the tag.
   APInt getTag(uint32_t BitWidth, MemoryObject *Obj);
@@ -234,6 +282,44 @@ class Context {
   void toBytes(const AnyValue &Val, Type *Ty, uint32_t OffsetInBits,
                MutableBytesView Bytes, bool PaddingBits);
 
+  /// Return whether \p Ancestor is on \p Descendant's noalias parent chain.
+  /// This relation defines whether an access is local to a protected node.
+  bool isNoAliasAncestor(uint64_t Ancestor, uint64_t Descendant) const;
+  bool hasActiveNoAliasDescendant(uint64_t NodeID) const;
+  /// Try to erase the node if it is inactive and has no active descendant.
+  void tryEraseInactiveNoAliasNode(uint64_t NodeID);
+  static StringRef getNoAliasAccessKindName(NoAliasAccessKind Kind);
+  static StringRef getNoAliasStateName(NoAliasState State);
+  static std::string getNoAliasNodeName(uint64_t NodeID);
+  static std::string getNoAliasObjectName(const MemoryObject &MO);
+  void appendNoAliasEvent(std::string Msg);
+  /// Record a noalias violation in both the user-facing error slot and verbose
+  /// event queue.
+  void setNoAliasViolation(uint64_t ProtectedNodeId, uint64_t AccessNode,
+                           const MemoryObject &MO, uint64_t Begin, uint64_t End,
+                           NoAliasAccessKind Kind, bool IsLocal,
+                           NoAliasState State, bool IsProtectorEndAction);
+  /// Apply the noalias state machine for one homogeneous byte range. Returns
+  /// std::nullopt when the access is forbidden and should be reported as an
+  /// immediate UB.
+  static std::optional<NoAliasState>
+  transitionNoAliasState(NoAliasState State, NoAliasAccessKind Kind,
+                         bool IsLocal);
+  /// Apply a memory access to every active protector for \p MO. \p
+  /// SkipDescendantsOf is used for protector-end synthetic accesses.
+  bool accessNoAliasImpl(MemoryObject &MO, uint64_t Offset, uint64_t Size,
+                         uint64_t AccessNode, NoAliasAccessKind Kind,
+                         uint64_t SkipDescendantsOf);
+  /// Update one node's sparse byte-state runs for access to [Begin, End).
+  /// The nodes store only non-Reserved runs, so this routine splits old runs,
+  /// treats gaps as implicit Reserved ranges, transitions each touches segment,
+  /// and coalesces adjacent ranges that end in the same non-Reserved state.
+  bool updateNoAliasNodeForAccess(NoAliasNode &Node, uint64_t Begin,
+                                  uint64_t End, NoAliasAccessKind Kind,
+                                  bool IsLocal, uint64_t ProtectedNodeID,
+                                  uint64_t AccessNode,
+                                  bool IsProtectorEndAction);
+
   // Constants
   // Use std::map to avoid iterator/reference invalidation.
   std::map<Constant *, AnyValue> ConstCache;
@@ -259,10 +345,12 @@ class Context {
   void setVScale(uint32_t VS) { VScale = VS; }
   void setMaxSteps(uint32_t MS) { MaxSteps = MS; }
   void setMaxStackDepth(uint32_t Depth) { MaxStackDepth = Depth; }
+  void setExperimentalNoAlias(bool Enabled) { ExperimentalNoAlias = Enabled; }
   uint64_t getMemoryLimit() const { return MaxMem; }
   uint32_t getVScale() const { return VScale; }
   uint32_t getMaxSteps() const { return MaxSteps; }
   uint32_t getMaxStackDepth() const { return MaxStackDepth; }
+  bool isExperimentalNoAliasEnabled() const { return ExperimentalNoAlias; }
   void setUndefValueBehavior(UndefValueBehavior UB) { UndefBehavior = UB; }
   void reseed(uint32_t Seed) { Rng.seed(Seed); }
 
@@ -319,6 +407,20 @@ class Context {
   Function *getTargetFunction(const Pointer &Ptr);
   BasicBlock *getTargetBlock(const Pointer &Ptr);
 
+  /// Create a new protected noalias node based on \p Ptr and return a pointer
+  /// associated with that node. The underlying pointer provenance is unchanged.
+  Pointer createNoAliasPointer(const Pointer &Ptr);
+  /// Apply a memory access to the active noalias state machines for \p MO.
+  /// Returns false when the protected state machine detects UB.
+  bool accessNoAlias(MemoryObject &MO, uint64_t Offset, uint64_t Size,
+                     uint64_t AccessNode, NoAliasAccessKind Kind);
+  /// End all noalias protectors created for a call frame.
+  bool endNoAliasScopes(ArrayRef<uint64_t> Nodes);
+  StringRef getLastNoAliasError() const { return LastNoAliasError; }
+  SmallVector<std::string, 4> takeNoAliasEvents();
+  /// Drop noalias state for an object \p MO that is no longer usable.
+  void clearNoAliasState(const MemoryObject &MO);
+
   /// Initialize global variables and function/block objects. This function
   /// should be called before executing any function. Returns false if the
   /// initialization fails (e.g., the memory limit is exceeded during
diff --git a/llvm/tools/llubi/lib/ExecutorBase.cpp b/llvm/tools/llubi/lib/ExecutorBase.cpp
index 5c1479d277bf9..7c58d0baa28c0 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.cpp
+++ b/llvm/tools/llubi/lib/ExecutorBase.cpp
@@ -13,18 +13,29 @@
 #include "ExecutorBase.h"
 
 namespace llvm::ubi {
-Frame::Frame(Function &F, CallBase *CallSite, Frame *LastFrame,
-             ArrayRef<AnyValue> Args, AnyValue &RetVal,
-             const TargetLibraryInfoImpl &TLIImpl)
+Frame::Frame(Context &Ctx, Function &F, CallBase *CallSite, Frame *LastFrame,
+             ArrayRef<AnyValue> Args, AnyValue &RetVal)
     : Func(F), LastFrame(LastFrame), CallSite(CallSite), Args(Args),
-      RetVal(RetVal), TLI(TLIImpl, &F) {
+      RetVal(RetVal), TLI(Ctx.getTLIImpl(), &F) {
   assert((Args.size() == F.arg_size() ||
           (F.isVarArg() && Args.size() >= F.arg_size())) &&
          "Expected enough arguments to call the function.");
   BB = &Func.getEntryBlock();
   PC = BB->begin();
-  for (Argument &Arg : F.args())
-    ValueMap[&Arg] = Args[Arg.getArgNo()];
+  for (Argument &Arg : F.args()) {
+    AnyValue ArgValue = Args[Arg.getArgNo()];
+    // Retag only callee-visible noalias pointer parameters. This creates the
+    // protected node for the dynamic call frame without changing the normal
+    // provenance carried by the pointer.
+    if (Ctx.isExperimentalNoAliasEnabled() && Arg.hasNoAliasAttr() &&
+        Arg.getType()->isPointerTy() && !ArgValue.isPoison()) {
+      Pointer Retagged = Ctx.createNoAliasPointer(ArgValue.asPointer());
+      if (uint64_t NodeID = Retagged.getNoAliasNodeID())
+        NoAliasNodes.push_back(NodeID);
+      ArgValue = Retagged;
+    }
+    ValueMap[&Arg] = std::move(ArgValue);
+  }
 }
 
 DiagnosticReporter ExecutorBase::reportImmediateUB() {
@@ -35,6 +46,11 @@ DiagnosticReporter ExecutorBase::reportError() {
   return DiagnosticReporter(*this, DiagnosticKind::Error);
 }
 
+void ExecutorBase::flushNoAliasEvents() {
+  for (const std::string &Msg : Ctx.takeNoAliasEvents())
+    Handler.onNoAliasEvent(Msg);
+}
+
 void ExecutorBase::reportImmediateUBString(StringRef Msg) {
   // Check if we have already reported an immediate UB.
   if (hasProgramExited())
@@ -127,6 +143,18 @@ AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
                             NoUndef ? &ContainsUndefinedBits : nullptr);
     if (NoUndef && ContainsUndefinedBits)
       reportImmediateUB() << "The value loaded contains undefined bits.";
+
+    // Run noalias after ordinary memory validity checks so diagnostics report
+    // aliasing only for otherwise valid concrete accesses.
+    if (const uint64_t AccessSize = Ctx.getEffectiveTypeStoreSize(ValTy);
+        !Ctx.accessNoAlias(*MO, *Offset, AccessSize, PtrVal.getNoAliasNodeID(),
+                           NoAliasAccessKind::Read)) {
+      flushNoAliasEvents();
+      reportImmediateUB() << Ctx.getLastNoAliasError();
+      return AnyValue::getPoisonValue(Ctx, ValTy);
+    }
+    flushNoAliasEvents();
+
     return Res;
   }
   return AnyValue::getPoisonValue(Ctx, ValTy);
@@ -146,11 +174,19 @@ void ExecutorBase::store(const AnyValue &Ptr, Align Alignment,
     return;
   }
   // TODO: pointer capability check
+  const uint64_t AccessSize = Ctx.getEffectiveTypeStoreSize(ValTy);
   if (auto Offset =
-          verifyMemAccess(*MO, PtrVal.address(),
-                          Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
-                          /*IsStore=*/true))
+          verifyMemAccess(*MO, PtrVal.address(), AccessSize, Alignment,
+                          /*IsStore=*/true)) {
+    if (!Ctx.accessNoAlias(*MO, *Offset, AccessSize, PtrVal.getNoAliasNodeID(),
+                           NoAliasAccessKind::Write)) {
+      flushNoAliasEvents();
+      reportImmediateUB() << Ctx.getLastNoAliasError();
+      return;
+    }
+    flushNoAliasEvents();
     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 d07b4dae1792e..14d029b963148 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.h
+++ b/llvm/tools/llubi/lib/ExecutorBase.h
@@ -58,6 +58,8 @@ struct Frame {
   // Stack objects allocated in this frame. They will be automatically freed
   // when the function returns.
   SmallVector<IntrusiveRefCntPtr<MemoryObject>> Allocas;
+  // Protected noalias nodes created for this frame's arguments
+  SmallVector<uint64_t, 4> NoAliasNodes;
   // Values of arguments and executed instructions in this function.
   DenseMap<Value *, AnyValue> ValueMap;
 
@@ -66,9 +68,8 @@ struct Frame {
   SmallVector<AnyValue> CalleeArgs;
   AnyValue CalleeRetVal;
 
-  Frame(Function &F, CallBase *CallSite, Frame *LastFrame,
-        ArrayRef<AnyValue> Args, AnyValue &RetVal,
-        const TargetLibraryInfoImpl &TLIImpl);
+  Frame(Context &Ctx, Function &F, CallBase *CallSite, Frame *LastFrame,
+        ArrayRef<AnyValue> Args, AnyValue &RetVal);
 };
 
 enum class DiagnosticKind {
@@ -99,6 +100,8 @@ class ExecutorBase {
   DiagnosticReporter reportImmediateUB();
   DiagnosticReporter reportError();
 
+  void flushNoAliasEvents();
+
   /// 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,
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index f50f074d9d457..72783b3dfef41 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -469,8 +469,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
                ArrayRef<AnyValue> Args, AnyValue &RetVal)
       : ExecutorBase(C, H), DL(Ctx.getDataLayout()),
         Lib(Ctx, Handler, DL, static_cast<ExecutorBase &>(*this)) {
-    CallStack.emplace_back(F, /*CallSite=*/nullptr, /*LastFrame=*/nullptr, Args,
-                           RetVal, Ctx.getTLIImpl());
+    CallStack.emplace_back(Ctx, F, /*CallSite=*/nullptr, /*LastFrame=*/nullptr,
+                           Args, RetVal);
   }
 
   void visitReturnInst(ReturnInst &RI) {
@@ -1292,8 +1292,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
       ArrayRef<AnyValue> Args = CurrentFrame->CalleeArgs;
       AnyValue &RetVal = CurrentFrame->CalleeRetVal;
       CurrentFrame->State = FrameState::Pending;
-      CallStack.emplace_back(*Callee, &CB, CurrentFrame, Args, RetVal,
-                             Ctx.getTLIImpl());
+      CallStack.emplace_back(Ctx, *Callee, &CB, CurrentFrame, Args, RetVal);
     }
   }
 
@@ -1805,6 +1804,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
       CurrentFrame = &Top;
       if (Top.State == FrameState::Entry) {
         Handler.onFunctionEntry(Top.Func, Top.Args, Top.CallSite);
+        flushNoAliasEvents();
       } else {
         assert(Top.State == FrameState::Pending &&
                "Expected to return from a callee.");
@@ -1844,6 +1844,14 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
       if (Top.State == FrameState::Exit) {
         assert((Top.Func.getReturnType()->isVoidTy() || !Top.RetVal.isNone()) &&
                "Expected return value to be set on function exit.");
+        // End protectors before freeing frame allocas: protector end actions
+        // are still accesses to the protected memory object.
+        if (!Ctx.endNoAliasScopes(Top.NoAliasNodes)) {
+          flushNoAliasEvents();
+          reportImmediateUB() << Ctx.getLastNoAliasError();
+          break;
+        }
+        flushNoAliasEvents();
         Handler.onFunctionExit(Top.Func, Top.RetVal);
         // Free stack objects allocated in this frame.
         for (auto &Obj : Top.Allocas)
diff --git a/llvm/tools/llubi/lib/Value.h b/llvm/tools/llubi/lib/Value.h
index 59d9a5c5f46fd..41b993413edb1 100644
--- a/llvm/tools/llubi/lib/Value.h
+++ b/llvm/tools/llubi/lib/Value.h
@@ -33,12 +33,14 @@ class AnyValue;
 struct Byte {
   uint8_t ConcreteMask;
   uint8_t Value;
-  uint8_t TagMask;  // A mask to indicate which bits are pointer bits.
-  uint8_t TagValue; // Part of the tag for provenance tracking of pointers.
+  uint8_t TagMask;      // A mask to indicate which bits are pointer bits.
+  uint8_t TagValue;     // Part of the tag for provenance tracking of pointers.
+  uint8_t NoAliasMask;  // A mask to indicate which bits carry a noalias node.
+  uint64_t NoAliasNode; // Shadow noalias node id for pointer bits.
 
-  static Byte poison() { return Byte{0, 0, 0, 0}; }
-  static Byte undef() { return Byte{0, 255, 0, 0}; }
-  static Byte concrete(uint8_t Val) { return Byte{255, Val, 0, 0}; }
+  static Byte poison() { return Byte{0, 0, 0, 0, 0, 0}; }
+  static Byte undef() { return Byte{0, 255, 0, 0, 0, 0}; }
+  static Byte concrete(uint8_t Val) { return Byte{255, Val, 0, 0, 0, 0}; }
 
   void zeroBits(uint8_t Mask) {
     ConcreteMask |= Mask;
@@ -59,6 +61,7 @@ struct Byte {
     ConcreteMask |= Mask;
     Value = (Value & ~Mask) | (Val & Mask);
     TagMask &= ~Mask;
+    NoAliasMask &= ~Mask;
   }
 
   void writeTagBits(uint8_t Mask, uint8_t Tag) {
@@ -69,25 +72,50 @@ struct Byte {
     TagValue = (TagValue & ~Mask) | (Tag & Mask);
   }
 
+  void writeNoAliasBits(uint8_t Mask, uint64_t NodeID) {
+    assert((ConcreteMask & Mask) == Mask &&
+           "Please ensure pointer bits are concrete before calling "
+           "writeNoAliasBits.");
+    if (!NodeID) {
+      NoAliasMask &= ~Mask;
+      return;
+    }
+    if (NoAliasMask && NoAliasNode != NodeID)
+      NoAliasMask = 0;
+    NoAliasNode = NodeID;
+    NoAliasMask |= Mask;
+  }
+
   /// Returns a logical byte that is part of two adjacent bytes.
   /// Example with ShAmt = 5:
   ///     |       Low       |      High       |
   /// LSB | 0 1 0 1 0 1 0 1 | 0 0 0 0 1 1 1 1 | MSB
   ///     Result =  | 1 0 1   0 0 0 0 1 |
   static Byte fshr(const Byte &Low, const Byte &High, uint32_t ShAmt) {
+    uint16_t NoAliasMask = Low.NoAliasMask | (High.NoAliasMask << 8);
+    uint64_t NoAliasNode = Low.NoAliasNode;
+    if (Low.NoAliasMask && High.NoAliasMask &&
+        Low.NoAliasNode != High.NoAliasNode)
+      NoAliasMask = 0;
+    else if (!Low.NoAliasMask)
+      NoAliasNode = High.NoAliasNode;
     return Byte{
         static_cast<uint8_t>((Low.ConcreteMask | (High.ConcreteMask << 8)) >>
                              ShAmt),
         static_cast<uint8_t>((Low.Value | (High.Value << 8)) >> ShAmt),
         static_cast<uint8_t>((Low.TagMask | (High.TagMask << 8)) >> ShAmt),
-        static_cast<uint8_t>((Low.TagValue | (High.TagValue << 8)) >> ShAmt)};
+        static_cast<uint8_t>((Low.TagValue | (High.TagValue << 8)) >> ShAmt),
+        static_cast<uint8_t>(NoAliasMask >> ShAmt),
+        NoAliasNode};
   }
 
   Byte lshr(uint8_t Shift) const {
     return Byte{static_cast<uint8_t>(ConcreteMask >> Shift),
                 static_cast<uint8_t>(Value >> Shift),
                 static_cast<uint8_t>(TagMask >> Shift),
-                static_cast<uint8_t>(TagValue >> Shift)};
+                static_cast<uint8_t>(TagValue >> Shift),
+                static_cast<uint8_t>(NoAliasMask >> Shift),
+                NoAliasNode};
   }
 };
 
@@ -111,20 +139,30 @@ class Pointer {
   // The address of the pointer. The bit width is determined by
   // DataLayout::getPointerSizeInBits.
   APInt Address;
+  // A side-channel id for the noalias borrow tree node this pointer is based
+  // on. This is intentionally separate from the object provenance above.
+  uint64_t NoAliasNode = 0;
   // TODO: modeling inrange(Start, End) attribute
 
 public:
   explicit Pointer(const APInt &Address) : Obj(nullptr), Address(Address) {}
   explicit Pointer(IntrusiveRefCntPtr<MemoryObject> Obj, const APInt &Address)
       : Obj(std::move(Obj)), Address(Address) {}
+  explicit Pointer(IntrusiveRefCntPtr<MemoryObject> Obj, const APInt &Address,
+                   uint64_t NoAliasNode)
+      : Obj(std::move(Obj)), Address(Address), NoAliasNode(NoAliasNode) {}
   Pointer getWithNewAddr(const APInt &NewAddr) const {
     return Pointer(Obj, NewAddr);
   }
+  Pointer getWithNoAliasNode(uint64_t NewNoAliasNode) const {
+    return Pointer(Obj, Address, NewNoAliasNode);
+  }
   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; }
   MemoryObject *getMemoryObject() const { return Obj.get(); }
+  uint64_t getNoAliasNodeID() const { return NoAliasNode; }
 };
 
 // Value representation for actual values of LLVM values.
diff --git a/llvm/tools/llubi/llubi.cpp b/llvm/tools/llubi/llubi.cpp
index 6e0616405b703..86fb91367cfab 100644
--- a/llvm/tools/llubi/llubi.cpp
+++ b/llvm/tools/llubi/llubi.cpp
@@ -79,6 +79,11 @@ static cl::opt<unsigned>
          cl::desc("Random seed for non-deterministic behavior (default = 0)"),
          cl::value_desc("N"), cl::init(0), cl::cat(InterpreterCategory));
 
+static cl::opt<bool>
+    ExperimentalNoAlias("experimental-noalias",
+                        cl::desc("Enable experimental LLVM noalias checking."),
+                        cl::init(false), cl::cat(InterpreterCategory));
+
 cl::opt<ubi::UndefValueBehavior> UndefBehavior(
     "", cl::desc("Choose undef value behavior:"),
     cl::values(clEnumVal(ubi::UndefValueBehavior::NonDeterministic,
@@ -151,6 +156,11 @@ class VerboseEventHandler : public ubi::EventHandler {
     llvm_unreachable("Unknown ProgramExitKind");
   }
 
+  bool onNoAliasEvent(StringRef Msg) override {
+    errs() << "NoAlias: " << Msg << '\n';
+    return true;
+  }
+
   void onUnrecognizedInstruction(Instruction &I) override {
     errs() << "Unrecognized instruction: " << I << '\n';
   }
@@ -207,6 +217,7 @@ int main(int argc, char **argv) {
   Ctx.setVScale(VScale);
   Ctx.setMaxSteps(MaxSteps);
   Ctx.setMaxStackDepth(MaxStackDepth);
+  Ctx.setExperimentalNoAlias(ExperimentalNoAlias);
   Ctx.setUndefValueBehavior(UndefBehavior);
   Ctx.reseed(Seed);
 

>From d5b1fca9fba4daeb328b87e70c447b483441e911 Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhigec_cpp at outlook.com>
Date: Tue, 5 May 2026 16:30:59 +0800
Subject: [PATCH 6/6] [llubi] Fix Pointer::getWithNewAddr

---
 .../test/tools/llubi/noalias_outside_range.ll | 24 ++++++++----------
 .../tools/llubi/noalias_overlapping_ranges.ll | 25 +++++++------------
 llvm/tools/llubi/lib/Value.h                  |  2 +-
 3 files changed, 20 insertions(+), 31 deletions(-)

diff --git a/llvm/test/tools/llubi/noalias_outside_range.ll b/llvm/test/tools/llubi/noalias_outside_range.ll
index aa946d0f11e84..173a9aaa4c259 100644
--- a/llvm/test/tools/llubi/noalias_outside_range.ll
+++ b/llvm/test/tools/llubi/noalias_outside_range.ll
@@ -36,21 +36,17 @@ define void @main() {
 ; CHECK-NEXT: NoAlias: write through raw/root on 'data' bytes [1, 2) checked 1 active noalias protector
 ; CHECK-NEXT:   store i8 42, ptr %raw1, align 1
 ; CHECK-NEXT:   %x2 = getelementptr i8, ptr %x, i64 2 => ptr 0xA [data + 2]
-; CHECK-NEXT: NoAlias: node #1 foreign read through raw/root on 'data' bytes [2, 3): Reserved -> ReservedF
-; CHECK-NEXT: NoAlias: read through raw/root on 'data' bytes [2, 3) checked 1 active noalias protector
+; CHECK-NEXT: NoAlias: node #1 local read through node #1 on 'data' bytes [2, 3): Reserved -> ReservedL
+; CHECK-NEXT: NoAlias: read through node #1 on 'data' bytes [2, 3) checked 1 active noalias protector
 ; CHECK-NEXT:   %v2 = load i8, ptr %x2, align 1 => i8 62
 ; CHECK-NEXT:   %x3 = getelementptr i8, ptr %x, i64 3 => ptr 0xB [data + 3]
-; CHECK-NEXT: NoAlias: node #1 foreign read through raw/root on 'data' bytes [3, 4): Reserved -> ReservedF
-; CHECK-NEXT: NoAlias: read through raw/root on 'data' bytes [3, 4) checked 1 active noalias protector
+; CHECK-NEXT: NoAlias: node #1 local read through node #1 on 'data' bytes [3, 4): Reserved -> ReservedL
+; CHECK-NEXT: NoAlias: read through node #1 on 'data' bytes [3, 4) checked 1 active noalias protector
 ; CHECK-NEXT:   %v3 = load i8, ptr %x3, align 1 => i8 -117
 ; CHECK-NEXT:   %raw3 = getelementptr i8, ptr %raw, i64 3 => ptr 0xB [data + 3]
-; CHECK-NEXT: NoAlias: node #1 foreign write through raw/root on 'data' bytes [3, 4): ReservedF -> Disabled
-; CHECK-NEXT: NoAlias: write through raw/root on 'data' bytes [3, 4) checked 1 active noalias protector
-; CHECK-NEXT:   store i8 42, ptr %raw3, align 1
-; CHECK-NEXT:   ret void
-; CHECK-NEXT: NoAlias: ended protector node #1
-; CHECK-NEXT: NoAlias: erased inactive protector node #1
-; CHECK-NEXT: Exiting function: stuff
-; CHECK-NEXT:   call void @stuff(ptr %raw, ptr %raw)
-; CHECK-NEXT:   ret void
-; CHECK-NEXT: Exiting function: main
+; CHECK-NEXT: NoAlias: noalias violation: write through raw/root on 'data' bytes [3, 4) is foreign to protected node #1, but that protector is in ReservedL state
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i8 42, ptr %raw3, align 1 at @stuff
+; CHECK-NEXT: #1   call void @stuff(ptr %raw, ptr %raw) at @main
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through raw/root on 'data' bytes [3, 4) is foreign to protected node #1, but that protector is in ReservedL state
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_overlapping_ranges.ll b/llvm/test/tools/llubi/noalias_overlapping_ranges.ll
index 068e4a9136ee9..c9e4d8815fb62 100644
--- a/llvm/test/tools/llubi/noalias_overlapping_ranges.ll
+++ b/llvm/test/tools/llubi/noalias_overlapping_ranges.ll
@@ -26,21 +26,14 @@ define void @main() {
 ; CHECK-NEXT: NoAlias: created protector node #1 for 'array' based on raw/root
 ; CHECK-NEXT: NoAlias: created protector node #2 for 'array' based on raw/root
 ; CHECK-NEXT:   %a1 = getelementptr [4 x i32], ptr %a, i64 0, i64 1 => ptr 0xC [array + 4]
-; CHECK-NEXT: NoAlias: node #1 foreign write through raw/root on 'array' bytes [4, 8): Reserved -> Disabled
-; CHECK-NEXT: NoAlias: node #2 foreign write through raw/root on 'array' bytes [4, 8): Reserved -> Disabled
-; CHECK-NEXT: NoAlias: write through raw/root on 'array' bytes [4, 8) checked 2 active noalias protectors
+; CHECK-NEXT: NoAlias: node #1 local write through node #1 on 'array' bytes [4, 8): Reserved -> Unique
+; CHECK-NEXT: NoAlias: node #2 foreign write through node #1 on 'array' bytes [4, 8): Reserved -> Disabled
+; CHECK-NEXT: NoAlias: write through node #1 on 'array' bytes [4, 8) checked 2 active noalias protectors
 ; CHECK-NEXT:   store i32 5, ptr %a1, align 4
 ; CHECK-NEXT:   %b1 = getelementptr [4 x i32], ptr %b, i64 0, i64 1 => ptr 0xC [array + 4]
-; CHECK-NEXT: NoAlias: node #1 foreign write through raw/root on 'array' bytes [4, 8): Disabled -> Disabled
-; CHECK-NEXT: NoAlias: node #2 foreign write through raw/root on 'array' bytes [4, 8): Disabled -> Disabled
-; CHECK-NEXT: NoAlias: write through raw/root on 'array' bytes [4, 8) checked 2 active noalias protectors
-; CHECK-NEXT:   store i32 6, ptr %b1, align 4
-; CHECK-NEXT:   ret void
-; CHECK-NEXT: NoAlias: ended protector node #1
-; CHECK-NEXT: NoAlias: erased inactive protector node #1
-; CHECK-NEXT: NoAlias: ended protector node #2
-; CHECK-NEXT: NoAlias: erased inactive protector node #2
-; CHECK-NEXT: Exiting function: write_overlap
-; CHECK-NEXT:   call void @write_overlap(ptr %array, ptr %array)
-; CHECK-NEXT:   ret void
-; CHECK-NEXT: Exiting function: main
+; CHECK-NEXT: NoAlias: noalias violation: write through node #2 on 'array' bytes [4, 8) is foreign to protected node #1, but that protector is in Unique state
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i32 6, ptr %b1, align 4 at @write_overlap
+; CHECK-NEXT: #1   call void @write_overlap(ptr %array, ptr %array) at @main
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through node #2 on 'array' bytes [4, 8) is foreign to protected node #1, but that protector is in Unique state
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/tools/llubi/lib/Value.h b/llvm/tools/llubi/lib/Value.h
index 41b993413edb1..1fcfcab87abbb 100644
--- a/llvm/tools/llubi/lib/Value.h
+++ b/llvm/tools/llubi/lib/Value.h
@@ -152,7 +152,7 @@ class Pointer {
                    uint64_t NoAliasNode)
       : Obj(std::move(Obj)), Address(Address), NoAliasNode(NoAliasNode) {}
   Pointer getWithNewAddr(const APInt &NewAddr) const {
-    return Pointer(Obj, NewAddr);
+    return Pointer(Obj, NewAddr, NoAliasNode);
   }
   Pointer getWithNoAliasNode(uint64_t NewNoAliasNode) const {
     return Pointer(Obj, Address, NewNoAliasNode);



More information about the llvm-commits mailing list