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

Zhige Chen via llvm-commits llvm-commits at lists.llvm.org
Sat Sep 26 02:58:16 PDT 2026


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

>From e5ddf0abc73ab68d060f0b61c4b38e26452f68bd 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 1/5] [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              | 482 +++++++++++++++++-
 llvm/tools/llubi/lib/Context.h                | 102 ++++
 llvm/tools/llubi/lib/ExecutorBase.cpp         |  51 +-
 llvm/tools/llubi/lib/ExecutorBase.h           |   9 +-
 llvm/tools/llubi/lib/Interpreter.cpp          |  16 +-
 llvm/tools/llubi/lib/Value.h                  |  67 ++-
 llvm/tools/llubi/llubi.cpp                    |  10 +
 16 files changed, 1086 insertions(+), 30 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 00000000000000..6738a951b53c6f
--- /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 00000000000000..c1fcd813c0211c
--- /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 00000000000000..65ce89ac44636c
--- /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 00000000000000..dd9030e9ea10f6
--- /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 00000000000000..b76effb09d746c
--- /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 00000000000000..aa946d0f11e84a
--- /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 00000000000000..068e4a9136ee90
--- /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 00000000000000..da83ef7d01659f
--- /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 00000000000000..671659b1759f79
--- /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 e78263a2bab76e..d723665f2b7eb0 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -14,6 +14,9 @@
 #include "llvm/IR/GetElementPtrTypeIterator.h"
 #include "llvm/IR/Instructions.h"
 #include "llvm/Support/MathExtras.h"
+#include "llvm/Support/raw_ostream.h"
+
+#include <algorithm>
 
 namespace llvm::ubi {
 
@@ -463,6 +466,10 @@ AnyValue Context::fromBytes(ConstBytesView Bytes, Type *Ty,
   SmallVector<APInt::WordType> 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].
@@ -526,6 +533,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;
@@ -563,7 +586,9 @@ AnyValue Context::fromBytes(ConstBytesView Bytes, Type *Ty,
   if (IsTagValid) {
     APInt Tag(NumBitsToExtract, RawTagBits);
     if (auto Prov = TaggedProvenances.lookup(Tag))
-      return Pointer(std::move(Prov), Bits);
+      return Pointer(std::move(Prov), Bits,
+                     IsNoAliasValid && LoadedNoAliasNode ? *LoadedNoAliasNode
+                                                         : 0);
   }
   return Pointer(Bits);
 }
@@ -665,7 +690,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;
@@ -697,6 +723,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()) {
@@ -711,19 +746,21 @@ 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();
     APInt Tag = getTag(AddressBits.getBitWidth(), Val.asPointer().provenance());
     if (NeedsPadding)
       Tag = Tag.zext(NewOffsetInBits - OffsetInBits);
+    uint64_t NoAliasNode =
+        ExperimentalNoAlias ? Val.asPointer().getNoAliasNodeID() : 0;
     WriteBits(NeedsPadding ? AddressBits.zext(NewOffsetInBits - OffsetInBits)
                            : AddressBits,
-              &Tag);
+              &Tag, NoAliasNode);
   } else if (Ty->isByteTy()) {
     assert(!PaddingBits &&
            "Non-vector-element cases should be handled by the fast path.");
@@ -1127,6 +1164,7 @@ bool Context::free(const MemoryObject &Obj) {
 
   UsedMem -= std::max(It->second->getSize(), static_cast<uint64_t>(1));
 
+  clearNoAliasState(*It->second);
   MemoryObject &MutableObj = *It->second;
   MutableObj.State = MemoryObjectState::Freed;
   MutableObj.Bytes.clear();
@@ -1338,4 +1376,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 571be140cc83f9..a8d968f5ca4eb0 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -19,6 +19,7 @@
 #include <map>
 #include <optional>
 #include <random>
+#include <string>
 
 namespace llvm::ubi {
 
@@ -101,6 +102,8 @@ struct ProgramExitInfo {
   }
 };
 
+enum class NoAliasAccessKind { Read, Write };
+
 class MemoryObject : public RefCountedBase<MemoryObject> {
   uint64_t Address;
   uint64_t Size;
@@ -178,6 +181,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();
@@ -237,6 +241,7 @@ class Context {
   UndefValueBehavior UndefBehavior = UndefValueBehavior::NonDeterministic;
   NaNPropagationBehavior NaNBehavior = NaNPropagationBehavior::NonDeterministic;
   bool FusedMultiplyAdd = false;
+  bool ExperimentalNoAlias = false;
 
   std::mt19937_64 Rng;
   /// Always returns a random APInt value. It is not controlled by
@@ -291,6 +296,49 @@ class Context {
 
   /// Get the tag for the given pointer provenance.
   APInt getTag(uint32_t BitWidth, Provenance &Prov);
+
+  /// 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;
+
   AnyValue fromBytes(ConstBytesView Bytes, Type *Ty, uint32_t OffsetInBits,
                      bool CheckPaddingBits, bool *ContainsUndefinedBits);
   void toBytes(const AnyValue &Val, Type *Ty, uint32_t OffsetInBits,
@@ -304,6 +352,44 @@ class Context {
                                const APInt &Scale, GEPNoWrapFlags Flags,
                                AnyValue &AccumulatedOffset);
 
+  /// 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 *, MaterializedConstant> ConstCache;
@@ -341,6 +427,7 @@ class Context {
   void setMaxSteps(uint32_t MS) { MaxSteps = MS; }
   void setMaxStackDepth(uint32_t Depth) { MaxStackDepth = Depth; }
   void setFusedMultiplyAdd(bool F) { FusedMultiplyAdd = F; }
+  void setExperimentalNoAlias(bool Enabled) { ExperimentalNoAlias = Enabled; }
   uint64_t getMemoryLimit() const { return MaxMem; }
   uint32_t getVScale() const { return VScale; }
   uint32_t getMaxSteps() const { return MaxSteps; }
@@ -351,6 +438,7 @@ class Context {
   UndefValueBehavior getEffectiveUndefValueBehavior() const;
   NaNPropagationBehavior getEffectiveNaNPropagationBehavior() const;
   bool fuseMultiplyAdd() const { return FusedMultiplyAdd; }
+  bool isExperimentalNoAliasEnabled() const { return ExperimentalNoAlias; }
   void setUndefValueBehavior(UndefValueBehavior UB) { UndefBehavior = UB; }
   void setNaNPropagationBehavior(NaNPropagationBehavior NaNBehav) {
     NaNBehavior = NaNBehav;
@@ -438,6 +526,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 0cbb38073f652a..5bc53de09362a3 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())
@@ -131,6 +147,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,8 +174,17 @@ void ExecutorBase::store(const AnyValue &Ptr, Align Alignment,
   if (auto [MO, Offset] = verifyMemAccess(
           PtrVal, Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
           /*IsStore=*/true);
-      MO)
+      MO) {
+    if (const uint64_t AccessSize = Ctx.getEffectiveTypeStoreSize(ValTy);
+        !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 ff8025d189615c..815ee7b5700fc4 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;
 
@@ -69,9 +71,8 @@ struct Frame {
   SmallVector<IntrusiveRefCntPtr<MemoryObject>> CalleeByValArgs;
   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 {
@@ -102,6 +103,8 @@ class ExecutorBase {
   DiagnosticReporter reportImmediateUB();
   DiagnosticReporter reportError();
 
+  void flushNoAliasEvents();
+
   /// Check if the upcoming memory access is valid. Returns the resolved memory
   /// object and offset if it is valid.
   std::pair<MemoryObject *, uint64_t> verifyMemAccess(const Pointer &Ptr,
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 0c7e74f52f6890..c159fea539b162 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -924,8 +924,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) {
@@ -2183,8 +2183,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);
     }
   }
 
@@ -2832,6 +2831,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.");
@@ -2872,6 +2872,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 b0f90aab2f1b59..739574e698d0b6 100644
--- a/llvm/tools/llubi/lib/Value.h
+++ b/llvm/tools/llubi/lib/Value.h
@@ -42,13 +42,15 @@ 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; // For each pointer bit, the corresponding bit of the tag
-                    // for provenance tracking.
+  uint8_t TagMask;      // A mask to indicate which bits are pointer bits.
+  uint8_t TagValue;     // For each pointer bit, the corresponding bit of the
+                        // tag for provenance tracking.
+  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;
@@ -72,6 +74,7 @@ struct Byte {
     ConcreteMask |= Mask;
     Value = (Value & ~Mask) | (Val & Mask);
     TagMask &= ~Mask;
+    NoAliasMask &= ~Mask;
   }
 
   void writeTagBits(uint8_t Mask, uint8_t Tag) {
@@ -87,6 +90,23 @@ struct Byte {
     Value = (Value & ~Mask) | (RHS.Value & Mask);
     TagMask = (TagMask & ~Mask) | (RHS.TagMask & Mask);
     TagValue = (TagValue & ~Mask) | (RHS.TagValue & Mask);
+    NoAliasMask = (NoAliasMask & ~Mask) | (RHS.NoAliasMask & Mask);
+    if (RHS.NoAliasMask & Mask)
+      NoAliasNode = RHS.NoAliasNode;
+  }
+
+  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.
@@ -95,26 +115,38 @@ 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) {
+    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};
   }
 
   Byte shl(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};
   }
 
   bool areHighBitsZExtd(uint8_t BitsFrom) const {
@@ -222,6 +254,9 @@ 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 pointer provenance above.
+  uint64_t NoAliasNode = 0;
 
 public:
   explicit Pointer(const APInt &Address)
@@ -230,17 +265,27 @@ class Pointer {
       : Prov(std::move(Prov)), Address(Address) {
     assert(this->Prov && "Invalid provenance.");
   }
+  explicit Pointer(IntrusiveRefCntPtr<Provenance> Prov, const APInt &Address,
+                   uint64_t NoAliasNode)
+      : Prov(std::move(Prov)), Address(Address), NoAliasNode(NoAliasNode) {
+    assert(this->Prov && "Invalid provenance.");
+  }
   Pointer getWithNewAddr(const APInt &NewAddr) const {
-    return Pointer(Prov, NewAddr);
+    return Pointer(Prov, NewAddr, NoAliasNode);
   }
   Pointer getWithNewProvenance(IntrusiveRefCntPtr<Provenance> NewProv) const {
-    return Pointer(NewProv, Address);
+    return Pointer(NewProv, Address, NoAliasNode);
+  }
+  Pointer getWithNoAliasNode(uint64_t NewNoAliasNode) const {
+    return Pointer(Prov, 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; }
   Provenance &provenance() const { return *Prov; }
+  MemoryObject *getMemoryObject() const { return Prov->getMemoryObject(); }
+  uint64_t getNoAliasNodeID() const { return NoAliasNode; }
 };
 
 /// Represents a scalar byte value. If the value is not byte-sized, the high
diff --git a/llvm/tools/llubi/llubi.cpp b/llvm/tools/llubi/llubi.cpp
index 00863dd5ecb391..6a628c9a79beb1 100644
--- a/llvm/tools/llubi/llubi.cpp
+++ b/llvm/tools/llubi/llubi.cpp
@@ -85,6 +85,11 @@ static cl::opt<bool>
                   cl::desc("Disable interpreter-introduced non-determinism."),
                   cl::init(false), cl::cat(InterpreterCategory));
 
+static cl::opt<bool>
+    ExperimentalNoAlias("experimental-noalias",
+                        cl::desc("Enable experimental LLVM noalias checking."),
+                        cl::init(false), cl::cat(InterpreterCategory));
+
 static cl::opt<bool> FuseFMulAdd("fuse-fmuladd",
                                  cl::desc("Fuse llvm.fmuladd.* intrinsic"),
                                  cl::init(true), cl::cat(InterpreterCategory));
@@ -196,6 +201,10 @@ class VerboseEventHandler : public NoopEventHandler {
 
     llvm_unreachable("Unknown ProgramExitKind");
   }
+  bool onNoAliasEvent(StringRef Msg) override {
+    errs() << "NoAlias: " << Msg << '\n';
+    return true;
+  }
 };
 
 int main(int argc, char **argv) {
@@ -258,6 +267,7 @@ int main(int argc, char **argv) {
   Ctx.setMaxStackDepth(MaxStackDepth);
   Ctx.setFusedMultiplyAdd(FuseFMulAdd);
   Ctx.setDeterministic(Deterministic);
+  Ctx.setExperimentalNoAlias(ExperimentalNoAlias);
   Ctx.setUndefValueBehavior(UndefBehavior);
   Ctx.setNaNPropagationBehavior(NaNPropagationBehavior);
   Ctx.reseed(Seed);

>From a138514c521b2195cd8f9ab89b3a6fe9ea92cdaa 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 2/5] [llubi] Fix Pointer::getWithNewAddr

---
 .../test/tools/llubi/noalias_outside_range.ll | 24 ++++++++----------
 .../tools/llubi/noalias_overlapping_ranges.ll | 25 +++++++------------
 2 files changed, 19 insertions(+), 30 deletions(-)

diff --git a/llvm/test/tools/llubi/noalias_outside_range.ll b/llvm/test/tools/llubi/noalias_outside_range.ll
index aa946d0f11e84a..173a9aaa4c2596 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 068e4a9136ee90..c9e4d8815fb629 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.

>From 744a309636431844508a39a73194a592dc39f147 Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhigec_cpp at outlook.com>
Date: Mon, 20 Jul 2026 14:36:54 +0800
Subject: [PATCH 3/5] [llubi] Refactor noalias implementation

---
 llvm/test/tools/llubi/noalias_after_return.ll |  10 +-
 .../tools/llubi/noalias_aliasing_reads.ll     |  22 +-
 .../tools/llubi/noalias_disjoint_intervals.ll |  22 +-
 .../tools/llubi/noalias_foreign_access.ll     |  14 +-
 .../tools/llubi/noalias_nested_activations.ll |  73 +++
 .../test/tools/llubi/noalias_outside_range.ll |  22 +-
 .../tools/llubi/noalias_overlapping_ranges.ll |  17 +-
 .../tools/llubi/noalias_prune_stale_node.ll   |  14 +-
 .../llubi/noalias_reserved_foreign_read.ll    |  14 +-
 llvm/tools/llubi/lib/Context.cpp              | 427 ++++++++----------
 llvm/tools/llubi/lib/Context.h                |  98 ++--
 llvm/tools/llubi/lib/ExecutorBase.cpp         |  11 +-
 llvm/tools/llubi/lib/ExecutorBase.h           |   4 +-
 llvm/tools/llubi/lib/Interpreter.cpp          |   8 +-
 14 files changed, 361 insertions(+), 395 deletions(-)
 create mode 100644 llvm/test/tools/llubi/noalias_nested_activations.ll

diff --git a/llvm/test/tools/llubi/noalias_after_return.ll b/llvm/test/tools/llubi/noalias_after_return.ll
index 6738a951b53c6f..0ad633eddf420d 100644
--- a/llvm/test/tools/llubi/noalias_after_return.ll
+++ b/llvm/test/tools/llubi/noalias_after_return.ll
@@ -18,15 +18,13 @@ define void @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: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
 ; 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: NoAlias: ended activation #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
diff --git a/llvm/test/tools/llubi/noalias_aliasing_reads.ll b/llvm/test/tools/llubi/noalias_aliasing_reads.ll
index c1fcd813c0211c..ab80f0ade27b68 100644
--- a/llvm/test/tools/llubi/noalias_aliasing_reads.ll
+++ b/llvm/test/tools/llubi/noalias_aliasing_reads.ll
@@ -20,26 +20,18 @@ define void @main() {
 ; 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: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 read through node #1 on 'a' bytes [0, 4): unaccessed -> reads by node #1
+; CHECK-NEXT: NoAlias: read through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
 ; 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: NoAlias: activation #1 read through node #2 on 'a' bytes [0, 4): reads by node #1 -> reads by multiple access classes
+; CHECK-NEXT: NoAlias: read through node #2 on 'a' bytes [0, 4) checked 1 active noalias activation
 ; 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: NoAlias: ended activation #1
 ; CHECK-NEXT: Exiting function: read_both
 ; CHECK-NEXT:   call void @read_both(ptr %a, ptr %a)
 ; CHECK-NEXT:   ret void
diff --git a/llvm/test/tools/llubi/noalias_disjoint_intervals.ll b/llvm/test/tools/llubi/noalias_disjoint_intervals.ll
index dd9030e9ea10f6..e328b7919e1869 100644
--- a/llvm/test/tools/llubi/noalias_disjoint_intervals.ll
+++ b/llvm/test/tools/llubi/noalias_disjoint_intervals.ll
@@ -22,26 +22,18 @@ define void @main() {
 ; 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: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
 ; 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: NoAlias: activation #1 write through node #2 on 'a' bytes [4, 8): unaccessed -> access including a write by node #2
+; CHECK-NEXT: NoAlias: write through node #2 on 'a' bytes [4, 8) checked 1 active noalias activation
 ; 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: NoAlias: ended activation #1
 ; 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 }
diff --git a/llvm/test/tools/llubi/noalias_foreign_access.ll b/llvm/test/tools/llubi/noalias_foreign_access.ll
index b76effb09d746c..927a43f9dcc498 100644
--- a/llvm/test/tools/llubi/noalias_foreign_access.ll
+++ b/llvm/test/tools/llubi/noalias_foreign_access.ll
@@ -20,13 +20,13 @@ define void @main() {
 ; 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: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
 ; 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: NoAlias: noalias violation: read through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
 ; 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: #0   %v = load i32, ptr %y, align 4 at @violates <stdin>:6
+; CHECK-NEXT: #1   call void @violates(ptr %a, ptr %a) at @main <stdin>:13
+; CHECK-NEXT: Immediate UB detected: noalias violation: read through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_nested_activations.ll b/llvm/test/tools/llubi/noalias_nested_activations.ll
new file mode 100644
index 00000000000000..1cd8cff29b6737
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_nested_activations.ll
@@ -0,0 +1,73 @@
+; 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 @write_inner(ptr noalias %x) {
+  store i32 1, ptr %x
+  ret void
+}
+
+define void @outer_ok(ptr noalias %x) {
+  store i32 0, ptr %x
+  call void @write_inner(ptr %x)
+  %v = load i32, ptr %x
+  ret void
+}
+
+define void @outer_bad(ptr noalias %x, ptr %raw) {
+  store i32 0, ptr %x
+  call void @write_inner(ptr %raw)
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  call void @outer_ok(ptr %a)
+  call void @outer_bad(ptr %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: Entering function: outer_ok
+; CHECK-NEXT:   ptr %x = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 0, ptr %x, align 4
+; CHECK-NEXT: Entering function: write_inner
+; CHECK-NEXT:   ptr %x = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' in activation #2 based on node #1
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): access including a write by node #1 -> access including a write by node #1
+; CHECK-NEXT: NoAlias: activation #2 write through node #2 on 'a' bytes [0, 4): unaccessed -> access including a write by node #2
+; CHECK-NEXT: NoAlias: write through node #2 on 'a' bytes [0, 4) checked 2 active noalias activations
+; CHECK-NEXT:   store i32 1, ptr %x, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: erased inactive protector node #2
+; CHECK-NEXT: NoAlias: ended activation #2
+; CHECK-NEXT: Exiting function: write_inner
+; CHECK-NEXT:   call void @write_inner(ptr %x)
+; CHECK-NEXT: NoAlias: activation #1 read through node #1 on 'a' bytes [0, 4): access including a write by node #1 -> access including a write by node #1
+; CHECK-NEXT: NoAlias: read through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   %v = load i32, ptr %x, align 4 => i32 1
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: erased inactive protector node #1
+; CHECK-NEXT: NoAlias: ended activation #1
+; CHECK-NEXT: Exiting function: outer_ok
+; CHECK-NEXT:   call void @outer_ok(ptr %a)
+; CHECK-NEXT: Entering function: outer_bad
+; CHECK-NEXT:   ptr %x = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %raw = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #3 for 'a' in activation #3 based on raw/root
+; CHECK-NEXT: NoAlias: activation #3 write through node #3 on 'a' bytes [0, 4): unaccessed -> access including a write by node #3
+; CHECK-NEXT: NoAlias: write through node #3 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 0, ptr %x, align 4
+; CHECK-NEXT: Entering function: write_inner
+; CHECK-NEXT:   ptr %x = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #4 for 'a' in activation #4 based on raw/root
+; CHECK-NEXT: NoAlias: noalias violation: write through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #3
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i32 1, ptr %x, align 4 at @write_inner <stdin>:5
+; CHECK-NEXT: #1   call void @write_inner(ptr %raw) at @outer_bad <stdin>:18
+; CHECK-NEXT: #2   call void @outer_bad(ptr %a, ptr %a) at @main <stdin>:25
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #3
+; 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
index 173a9aaa4c2596..0db9d92ad235da 100644
--- a/llvm/test/tools/llubi/noalias_outside_range.ll
+++ b/llvm/test/tools/llubi/noalias_outside_range.ll
@@ -30,23 +30,23 @@ define void @main() {
 ; 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: NoAlias: created protector node #1 for 'data' in activation #1 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: NoAlias: activation #1 write through raw/root on 'data' bytes [1, 2): unaccessed -> access including a write by raw/root
+; CHECK-NEXT: NoAlias: write through raw/root on 'data' bytes [1, 2) checked 1 active noalias activation
 ; 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 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: NoAlias: activation #1 read through node #1 on 'data' bytes [2, 3): unaccessed -> reads by node #1
+; CHECK-NEXT: NoAlias: read through node #1 on 'data' bytes [2, 3) checked 1 active noalias activation
 ; 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 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: NoAlias: activation #1 read through node #1 on 'data' bytes [3, 4): unaccessed -> reads by node #1
+; CHECK-NEXT: NoAlias: read through node #1 on 'data' bytes [3, 4) checked 1 active noalias activation
 ; 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: 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: NoAlias: noalias violation: write through raw/root on 'data' bytes [3, 4) combines multiple access classes with a write in activation #1
 ; 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: #0   store i8 42, ptr %raw3, align 1 at @stuff <stdin>:16
+; CHECK-NEXT: #1   call void @stuff(ptr %raw, ptr %raw) at @main <stdin>:23
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through raw/root on 'data' bytes [3, 4) combines multiple access classes with a write in activation #1
 ; 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 c9e4d8815fb629..511fd8d6eae32e 100644
--- a/llvm/test/tools/llubi/noalias_overlapping_ranges.ll
+++ b/llvm/test/tools/llubi/noalias_overlapping_ranges.ll
@@ -23,17 +23,16 @@ define void @main() {
 ; 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: NoAlias: created protector node #1 for 'array' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #2 for 'array' in activation #1 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 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: NoAlias: activation #1 write through node #1 on 'array' bytes [4, 8): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'array' bytes [4, 8) checked 1 active noalias activation
 ; 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: 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: NoAlias: noalias violation: write through node #2 on 'array' bytes [4, 8) combines multiple access classes with a write in activation #1
 ; 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: #0   store i32 6, ptr %b1, align 4 at @write_overlap <stdin>:11
+; CHECK-NEXT: #1   call void @write_overlap(ptr %array, ptr %array) at @main <stdin>:17
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through node #2 on 'array' bytes [4, 8) combines multiple access classes with a write in activation #1
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_prune_stale_node.ll b/llvm/test/tools/llubi/noalias_prune_stale_node.ll
index da83ef7d01659f..1d91bd9022b90c 100644
--- a/llvm/test/tools/llubi/noalias_prune_stale_node.ll
+++ b/llvm/test/tools/llubi/noalias_prune_stale_node.ll
@@ -26,25 +26,23 @@ define void @main() {
 ; 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: NoAlias: created protector node #1 for 'a' in activation #1 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: NoAlias: ended activation #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: NoAlias: created protector node #2 for 'a' in activation #2 based on raw/root
+; CHECK-NEXT: NoAlias: activation #2 write through node #2 on 'a' bytes [0, 4): unaccessed -> access including a write by node #2
+; CHECK-NEXT: NoAlias: write through node #2 on 'a' bytes [0, 4) checked 1 active noalias activation
 ; 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: NoAlias: ended activation #2
 ; CHECK-NEXT: Exiting function: write_one
 ; CHECK-NEXT:   call void @write_one(ptr %stale)
 ; CHECK-NEXT:   ret void
diff --git a/llvm/test/tools/llubi/noalias_reserved_foreign_read.ll b/llvm/test/tools/llubi/noalias_reserved_foreign_read.ll
index 671659b1759f79..71309741953a3d 100644
--- a/llvm/test/tools/llubi/noalias_reserved_foreign_read.ll
+++ b/llvm/test/tools/llubi/noalias_reserved_foreign_read.ll
@@ -23,13 +23,13 @@ define void @main() {
 ; 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: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 read through raw/root on 'a' bytes [0, 4): unaccessed -> reads by raw/root
+; CHECK-NEXT: NoAlias: read through raw/root on 'a' bytes [0, 4) checked 1 active noalias activation
 ; 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: NoAlias: noalias violation: write through node #1 on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
 ; 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: #0   store i32 1, ptr %x, align 4 at @foreign_read_then_local_write <stdin>:9
+; CHECK-NEXT: #1   call void @foreign_read_then_local_write(ptr %a, ptr %a) at @main <stdin>:16
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through node #1 on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
 ; 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 d723665f2b7eb0..6aec7e413068f3 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -1430,26 +1430,6 @@ StringRef Context::getNoAliasAccessKindName(NoAliasAccessKind Kind) {
   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";
@@ -1459,6 +1439,13 @@ std::string Context::getNoAliasNodeName(uint64_t NodeID) {
   return S;
 }
 
+std::string Context::getNoAliasActivationName(uint64_t ActivationID) {
+  std::string S;
+  raw_string_ostream OS(S);
+  OS << "activation #" << ActivationID;
+  return S;
+}
+
 std::string Context::getNoAliasObjectName(const MemoryObject &MO) {
   if (MO.getName().empty()) {
     std::string S;
@@ -1474,222 +1461,172 @@ 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))
+uint64_t Context::classifyNoAliasAccess(const NoAliasActivation &Activation,
+                                        const MemoryObject &MO,
+                                        uint64_t AccessNode) const {
+  for (uint64_t NodeID : Activation.Nodes) {
+    const auto It = NoAliasNodes.find(NodeID);
+    if (It == NoAliasNodes.end() || !It->second.Active ||
+        It->second.Object != &MO)
       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));
+    if (isNoAliasAncestor(NodeID, AccessNode))
+      return NodeID;
   }
-  return true;
+  return 0;
 }
 
-bool Context::updateNoAliasNodeForAccess(NoAliasNode &Node, uint64_t Begin,
-                                         uint64_t End, NoAliasAccessKind Kind,
-                                         bool IsLocal, uint64_t ProtectedNodeID,
-                                         uint64_t AccessNode,
-                                         bool IsProtectorEndAction) {
+bool Context::updateNoAliasAccesses(NoAliasActivation &Activation,
+                                    MemoryObject &MO, uint64_t Begin,
+                                    uint64_t End, uint64_t AccessClass,
+                                    NoAliasAccessKind Kind,
+                                    uint64_t ActivationID) {
   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)
+  SmallVector<NoAliasAccessRun, 4> NewRuns;
+  auto AppendRun = [&](uint64_t RunBegin, uint64_t RunEnd,
+                       NoAliasAccessSummary Summary) {
+    if (RunBegin == RunEnd)
       return;
     if (!NewRuns.empty() && NewRuns.back().End == RunBegin &&
-        NewRuns.back().State == State) {
+        NewRuns.back().Summary == Summary) {
       NewRuns.back().End = RunEnd;
       return;
     }
-    NewRuns.push_back({RunBegin, RunEnd, State});
+    NewRuns.push_back({RunBegin, RunEnd, Summary});
+  };
+
+  auto DescribeSummary = [&](raw_ostream &OS,
+                             const NoAliasAccessSummary &Summary) {
+    if (Summary.MultipleClasses) {
+      OS << "reads by multiple access classes";
+      return;
+    }
+    OS << (Summary.HasWrite ? "access including a write by " : "reads by ")
+       << getNoAliasNodeName(Summary.AccessClass);
   };
 
   auto AppendTransitioned = [&](uint64_t RunBegin, uint64_t RunEnd,
-                                NoAliasState State) -> bool {
+                                const NoAliasAccessSummary *Old) -> 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);
+
+    const bool IsWrite = Kind == NoAliasAccessKind::Write;
+    NoAliasAccessSummary New{AccessClass, false, IsWrite};
+    if (Old) {
+      New = *Old;
+      if (Old->MultipleClasses) {
+        if (IsWrite)
+          New.HasWrite = true;
+      } else if (Old->AccessClass == AccessClass) {
+        New.HasWrite |= IsWrite;
+      } else if (Old->HasWrite || IsWrite) {
+        New.MultipleClasses = true;
+        New.HasWrite = true;
+      } else {
+        New.AccessClass = 0;
+        New.MultipleClasses = true;
+      }
+    }
+
+    if (New.MultipleClasses && New.HasWrite) {
+      std::string S;
+      raw_string_ostream OS(S);
+      OS << "noalias violation: " << getNoAliasAccessKindName(Kind)
+         << " through " << getNoAliasNodeName(AccessClass) << " on "
+         << getNoAliasObjectName(MO) << " bytes [" << RunBegin << ", " << RunEnd
+         << ") combines multiple access classes with a write in "
+         << getNoAliasActivationName(ActivationID);
+      LastNoAliasError = std::move(S);
+      appendNoAliasEvent(LastNoAliasError);
       return false;
     }
+
     std::string S;
     raw_string_ostream OS(S);
-    if (IsProtectorEndAction)
-      OS << "protector end: ";
-    OS << getNoAliasNodeName(ProtectedNodeID) << ' '
-       << (IsLocal ? "local" : "foreign") << ' '
+    OS << getNoAliasActivationName(ActivationID) << ' '
        << getNoAliasAccessKindName(Kind) << " through "
-       << getNoAliasNodeName(AccessNode) << " on "
-       << getNoAliasObjectName(*Node.Object) << " bytes [" << RunBegin << ", "
-       << RunEnd << "): " << getNoAliasStateName(State) << " -> "
-       << getNoAliasStateName(*NewState);
+       << getNoAliasNodeName(AccessClass) << " on " << getNoAliasObjectName(MO)
+       << " bytes [" << RunBegin << ", " << RunEnd << "): ";
+    if (Old)
+      DescribeSummary(OS, *Old);
+    else
+      OS << "unaccessed";
+    OS << " -> ";
+    DescribeSummary(OS, New);
     appendNoAliasEvent(std::move(S));
-    AppendRun(RunBegin, RunEnd, *NewState);
+    AppendRun(RunBegin, RunEnd, New);
     return true;
   };
 
+  auto &Runs = Activation.Accesses[&MO];
   uint64_t Cur = Begin;
   bool InsertedAccessTail = false;
-  for (const NoAliasStateRun &Run : Node.States) {
+  for (const NoAliasAccessRun &Run : Runs) {
     if (Run.End <= Begin) {
-      AppendRun(Run.Begin, Run.End, Run.State);
+      AppendRun(Run.Begin, Run.End, Run.Summary);
       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))
+        if (!AppendTransitioned(Cur, End, nullptr))
           return false;
         InsertedAccessTail = true;
       }
-      AppendRun(Run.Begin, Run.End, Run.State);
+      AppendRun(Run.Begin, Run.End, Run.Summary);
       continue;
     }
 
     if (Run.Begin < Begin)
-      AppendRun(Run.Begin, Begin, Run.State);
+      AppendRun(Run.Begin, Begin, Run.Summary);
 
     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))
+    if (!AppendTransitioned(Cur, OverlapBegin, nullptr))
       return false;
 
     const uint64_t OverlapEnd = std::min(End, Run.End);
-    if (!AppendTransitioned(OverlapBegin, OverlapEnd, Run.State))
+    if (!AppendTransitioned(OverlapBegin, OverlapEnd, &Run.Summary))
       return false;
     Cur = OverlapEnd;
 
     if (Run.End > End) {
-      AppendRun(End, Run.End, Run.State);
+      AppendRun(End, Run.End, Run.Summary);
       InsertedAccessTail = true;
     }
   }
   if (!InsertedAccessTail) {
-    if (!AppendTransitioned(Cur, End, NoAliasState::Reserved))
+    if (!AppendTransitioned(Cur, End, nullptr))
       return false;
   }
 
-  Node.States = std::move(NewRuns);
+  Runs = std::move(NewRuns);
   return true;
 }
 
-Pointer Context::createNoAliasPointer(const Pointer &Ptr) {
+uint64_t Context::beginNoAliasActivation() {
   if (!ExperimentalNoAlias)
+    return 0;
+  const uint64_t ActivationID = NextNoAliasActivation++;
+  NoAliasActivations.try_emplace(ActivationID);
+  return ActivationID;
+}
+
+Pointer Context::createNoAliasPointer(const Pointer &Ptr,
+                                      uint64_t ActivationID) {
+  if (!ExperimentalNoAlias || !ActivationID)
     return Ptr;
 
   MemoryObject *MO = Ptr.getMemoryObject();
   if (!MO)
     return Ptr;
 
+  auto ActivationIt = NoAliasActivations.find(ActivationID);
+  assert(ActivationIt != NoAliasActivations.end() &&
+         "Noalias activation must be live before creating a parameter node.");
+
   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 the parent node was pruned after its activation ended, the incoming
+  // pointer is treated as a raw/root-derived pointer for this new activation.
   if (Parent && NoAliasNodes.find(Parent) == NoAliasNodes.end())
     Parent = 0;
   NoAliasNode Node;
@@ -1697,92 +1634,86 @@ Pointer Context::createNoAliasPointer(const Pointer &Ptr) {
   Node.Object = MO;
   Node.Active = true;
   NoAliasNodes.try_emplace(NodeID, std::move(Node));
-  NoAliasNodesByObject[MO].push_back(NodeID);
-  ++ActiveNoAliasScopes;
+  ActivationIt->second.Nodes.push_back(NodeID);
+
+  auto &Activations = NoAliasActivationsByObject[MO];
+  if (std::find(Activations.begin(), Activations.end(), ActivationID) ==
+      Activations.end())
+    Activations.push_back(ActivationID);
+
   std::string S;
   raw_string_ostream OS(S);
   OS << "created protector " << getNoAliasNodeName(NodeID) << " for "
-     << getNoAliasObjectName(*MO) << " based on " << getNoAliasNodeName(Parent);
+     << getNoAliasObjectName(*MO) << " in "
+     << getNoAliasActivationName(ActivationID) << " 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)
+  if (!ExperimentalNoAlias || !Size)
+    return true;
+
+  auto It = NoAliasActivationsByObject.find(&MO);
+  if (It == NoAliasActivationsByObject.end())
     return true;
 
-  return accessNoAliasImpl(MO, Offset, Size, AccessNode, Kind,
-                           /*SkipDescendantsOf=*/0);
+  const uint64_t End = Offset + Size;
+  uint32_t CheckedActivations = 0;
+  for (uint64_t ActivationID : It->second) {
+    auto ActivationIt = NoAliasActivations.find(ActivationID);
+    if (ActivationIt == NoAliasActivations.end())
+      continue;
+    ++CheckedActivations;
+    const uint64_t AccessClass =
+        classifyNoAliasAccess(ActivationIt->second, MO, AccessNode);
+    if (!updateNoAliasAccesses(ActivationIt->second, MO, Offset, End,
+                               AccessClass, Kind, ActivationID))
+      return false;
+  }
+
+  if (CheckedActivations) {
+    std::string S;
+    raw_string_ostream OS(S);
+    OS << getNoAliasAccessKindName(Kind) << " through "
+       << getNoAliasNodeName(AccessNode) << " on " << getNoAliasObjectName(MO)
+       << " bytes [" << Offset << ", " << End << ") checked "
+       << CheckedActivations << " active noalias activation"
+       << (CheckedActivations == 1 ? "" : "s");
+    appendNoAliasEvent(std::move(S));
+  }
+  return true;
 }
 
-bool Context::endNoAliasScopes(ArrayRef<uint64_t> Nodes) {
+void Context::endNoAliasActivation(uint64_t ActivationID) {
   if (!ExperimentalNoAlias)
-    return true;
+    return;
+
+  auto ActivationIt = NoAliasActivations.find(ActivationID);
+  if (ActivationIt == NoAliasActivations.end())
+    return;
 
+  SmallVector<uint64_t, 4> Nodes(ActivationIt->second.Nodes.begin(),
+                                 ActivationIt->second.Nodes.end());
   for (uint64_t NodeID : Nodes) {
-    auto It = NoAliasNodes.find(NodeID);
-    if (It == NoAliasNodes.end() || !It->second.Active)
+    auto NodeIt = NoAliasNodes.find(NodeID);
+    if (NodeIt == NoAliasNodes.end())
       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));
+    MemoryObject *MO = NodeIt->second.Object;
+    NodeIt->second.Active = false;
     tryEraseInactiveNoAliasNode(NodeID);
+    auto ObjectIt = NoAliasActivationsByObject.find(MO);
+    if (ObjectIt == NoAliasActivationsByObject.end())
+      continue;
+    auto &IDs = ObjectIt->second;
+    IDs.erase(std::remove(IDs.begin(), IDs.end(), ActivationID), IDs.end());
+    if (IDs.empty())
+      NoAliasActivationsByObject.erase(ObjectIt);
   }
-  return true;
+  appendNoAliasEvent("ended " + getNoAliasActivationName(ActivationID));
+  NoAliasActivations.erase(ActivationIt);
 }
 
 SmallVector<std::string, 4> Context::takeNoAliasEvents() {
@@ -1795,19 +1726,23 @@ void Context::clearNoAliasState(const MemoryObject &MO) {
   if (!ExperimentalNoAlias)
     return;
 
-  const auto It = NoAliasNodesByObject.find(&MO);
-  if (It == NoAliasNodesByObject.end())
+  const auto It = NoAliasActivationsByObject.find(&MO);
+  if (It == NoAliasActivationsByObject.end())
     return;
-  for (uint64_t NodeID : It->second) {
-    auto NodeIt = NoAliasNodes.find(NodeID);
-    if (NodeIt == NoAliasNodes.end() || !NodeIt->second.Active)
+  SmallVector<uint64_t, 4> ActivationIDs(It->second.begin(), It->second.end());
+  for (uint64_t ActivationID : ActivationIDs) {
+    auto ActivationIt = NoAliasActivations.find(ActivationID);
+    if (ActivationIt == NoAliasActivations.end())
       continue;
-    NodeIt->second.Active = false;
-    NodeIt->second.States.clear();
-    assert(ActiveNoAliasScopes && "mismatched noalias protector count");
-    --ActiveNoAliasScopes;
+    for (uint64_t NodeID : ActivationIt->second.Nodes) {
+      auto NodeIt = NoAliasNodes.find(NodeID);
+      if (NodeIt != NoAliasNodes.end() && NodeIt->second.Object == &MO)
+        NodeIt->second.Active = false;
+      tryEraseInactiveNoAliasNode(NodeID);
+    }
+    ActivationIt->second.Accesses.erase(const_cast<MemoryObject *>(&MO));
   }
-  NoAliasNodesByObject.erase(It);
+  NoAliasActivationsByObject.erase(It);
 }
 
 } // namespace llvm::ubi
diff --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index a8d968f5ca4eb0..d4195402f75f36 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -297,43 +297,47 @@ class Context {
   /// Get the tag for the given pointer provenance.
   APInt getTag(uint32_t BitWidth, Provenance &Prov);
 
-  /// 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,
+  /// Summary of the access classes that touched a byte range during one
+  /// dynamic function activation. Multiple access classes are permitted only
+  /// while every access is a read.
+  struct NoAliasAccessSummary {
+    uint64_t AccessClass = 0;
+    bool MultipleClasses = false;
+    bool HasWrite = false;
+
+    bool operator==(const NoAliasAccessSummary &Other) const {
+      return AccessClass == Other.AccessClass &&
+             MultipleClasses == Other.MultipleClasses &&
+             HasWrite == Other.HasWrite;
+    }
   };
 
-  /// A non-Reserved state over the byte interval [Begin, End).
-  struct NoAliasStateRun {
+  struct NoAliasAccessRun {
     uint64_t Begin;
     uint64_t End;
-    NoAliasState State;
+    NoAliasAccessSummary Summary;
   };
 
-  /// 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.
+  /// A noalias access class created by retagging a function argument. Parent is
+  /// another node, or 0 for the raw/root parent. Nodes carry no memory state;
+  /// they are used only to classify accesses in active function activations.
   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.
+  struct NoAliasActivation {
+    SmallVector<uint64_t, 4> Nodes;
+    DenseMap<MemoryObject *, SmallVector<NoAliasAccessRun, 1>> Accesses;
+  };
+
+  // ID 0 is reserved for the raw/root access class and for no activation.
   uint64_t NextNoAliasNode = 1;
-  uint64_t ActiveNoAliasScopes = 0;
+  uint64_t NextNoAliasActivation = 1;
   DenseMap<uint64_t, NoAliasNode> NoAliasNodes;
-  DenseMap<MemoryObject *, SmallVector<uint64_t, 2>> NoAliasNodesByObject;
+  DenseMap<uint64_t, NoAliasActivation> NoAliasActivations;
+  DenseMap<MemoryObject *, SmallVector<uint64_t, 2>> NoAliasActivationsByObject;
 
   // noalias-related diagnostics
   std::string LastNoAliasError;
@@ -359,36 +363,16 @@ class Context {
   /// 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 getNoAliasActivationName(uint64_t ActivationID);
   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);
+  uint64_t classifyNoAliasAccess(const NoAliasActivation &Activation,
+                                 const MemoryObject &MO,
+                                 uint64_t AccessNode) const;
+  bool updateNoAliasAccesses(NoAliasActivation &Activation, MemoryObject &MO,
+                             uint64_t Begin, uint64_t End, uint64_t AccessClass,
+                             NoAliasAccessKind Kind, uint64_t ActivationID);
 
   // Constants
   // Use std::map to avoid iterator/reference invalidation.
@@ -526,15 +510,15 @@ 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.
+  /// Begin one dynamic function activation containing noalias parameters.
+  uint64_t beginNoAliasActivation();
+  /// Create an access class for a noalias parameter in \p ActivationID.
+  Pointer createNoAliasPointer(const Pointer &Ptr, uint64_t ActivationID);
+  /// Apply an access to every active activation protecting \p MO.
   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);
+  /// End an activation and discard its access summaries.
+  void endNoAliasActivation(uint64_t ActivationID);
   StringRef getLastNoAliasError() const { return LastNoAliasError; }
   SmallVector<std::string, 4> takeNoAliasEvents();
   /// Drop noalias state for an object \p MO that is no longer usable.
diff --git a/llvm/tools/llubi/lib/ExecutorBase.cpp b/llvm/tools/llubi/lib/ExecutorBase.cpp
index 5bc53de09362a3..fc634830aa0892 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.cpp
+++ b/llvm/tools/llubi/lib/ExecutorBase.cpp
@@ -28,11 +28,12 @@ Frame::Frame(Context &Ctx, Function &F, CallBase *CallSite, Frame *LastFrame,
     // 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;
+        Arg.getType()->isPointerTy() && !ArgValue.isPoison() &&
+        ArgValue.asPointer().getMemoryObject()) {
+      if (!NoAliasActivation)
+        NoAliasActivation = Ctx.beginNoAliasActivation();
+      ArgValue =
+          Ctx.createNoAliasPointer(ArgValue.asPointer(), NoAliasActivation);
     }
     ValueMap[&Arg] = std::move(ArgValue);
   }
diff --git a/llvm/tools/llubi/lib/ExecutorBase.h b/llvm/tools/llubi/lib/ExecutorBase.h
index 815ee7b5700fc4..370808d77814b0 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.h
+++ b/llvm/tools/llubi/lib/ExecutorBase.h
@@ -58,8 +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;
+  // Dynamic noalias activation shared by this frame's noalias parameters.
+  uint64_t NoAliasActivation = 0;
   // Values of arguments and executed instructions in this function.
   DenseMap<Value *, AnyValue> ValueMap;
 
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index c159fea539b162..e15f4f0b197e4a 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -2872,13 +2872,7 @@ 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;
-        }
+        Ctx.endNoAliasActivation(Top.NoAliasActivation);
         flushNoAliasEvents();
         Handler.onFunctionExit(Top.Func, Top.RetVal);
         // Free stack objects allocated in this frame.

>From f14cda8259e927e92b0840f46fc9b51f20c9b320 Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhigec_cpp at outlook.com>
Date: Sat, 26 Sep 2026 14:45:10 +0800
Subject: [PATCH 4/5] [llubi] Refine noalias semantic and add more tests.

---
 llvm/test/tools/llubi/noalias_after_return.ll |  33 -----
 .../test/tools/llubi/noalias_aliasing_mut1.ll |  40 ++++++
 .../test/tools/llubi/noalias_aliasing_mut2.ll |  39 ++++++
 .../tools/llubi/noalias_aliasing_reads.ll     |   2 -
 .../tools/llubi/noalias_aliasing_unused.ll    |  60 ++++++++
 llvm/test/tools/llubi/noalias_byval.ll        |  34 +++++
 llvm/test/tools/llubi/noalias_callsite.ll     |  32 +++++
 .../tools/llubi/noalias_callsite_indirect.ll  |  34 +++++
 .../tools/llubi/noalias_callsite_intrinsic.ll |  25 ++++
 .../tools/llubi/noalias_disjoint_intervals.ll |   2 -
 .../tools/llubi/noalias_escaped_pointer.ll    |  84 +++++++++++
 llvm/test/tools/llubi/noalias_free.ll         |  35 +++++
 llvm/test/tools/llubi/noalias_histogram.ll    |  33 +++++
 llvm/test/tools/llubi/noalias_libc_read.ll    |  32 +++++
 llvm/test/tools/llubi/noalias_memcpy.ll       |  39 ++++++
 .../tools/llubi/noalias_memcpy_destination.ll |  43 ++++++
 llvm/test/tools/llubi/noalias_memmove.ll      |  36 +++++
 .../llubi/noalias_memory_effects_valid.ll     | 131 ++++++++++++++++++
 llvm/test/tools/llubi/noalias_memset.ll       |  34 +++++
 .../test/tools/llubi/noalias_memset_inline.ll |  34 +++++
 .../tools/llubi/noalias_nested_activations.ll |   2 -
 .../test/tools/llubi/noalias_outside_range.ll |   8 +-
 .../noalias_outside_range_foreign_first.ll    |  40 ++++++
 .../llubi/noalias_outside_range_unused.ll     |  50 +++++++
 .../tools/llubi/noalias_prune_stale_node.ll   |  49 -------
 .../tools/llubi/noalias_returned_pointer.ll   |  64 +++++++++
 ...noalias_returned_pointer_foreign_access.ll |  44 ++++++
 llvm/test/tools/llubi/noalias_wildcard.ll     |  36 +++++
 .../llubi/noalias_wildcard_foreign_first.ll   |  36 +++++
 .../noalias_wildcard_outside_allocation.ll    |  40 ++++++
 .../tools/llubi/noalias_wildcard_valid.ll     |  69 +++++++++
 llvm/tools/llubi/lib/Context.cpp              |  94 +++++--------
 llvm/tools/llubi/lib/Context.h                |  11 +-
 llvm/tools/llubi/lib/ExecutorBase.cpp         |  76 +++++-----
 llvm/tools/llubi/lib/ExecutorBase.h           |  16 ++-
 llvm/tools/llubi/lib/Interpreter.cpp          |  11 ++
 llvm/tools/llubi/lib/Library.cpp              |   4 +
 37 files changed, 1261 insertions(+), 191 deletions(-)
 delete mode 100644 llvm/test/tools/llubi/noalias_after_return.ll
 create mode 100644 llvm/test/tools/llubi/noalias_aliasing_mut1.ll
 create mode 100644 llvm/test/tools/llubi/noalias_aliasing_mut2.ll
 create mode 100644 llvm/test/tools/llubi/noalias_aliasing_unused.ll
 create mode 100644 llvm/test/tools/llubi/noalias_byval.ll
 create mode 100644 llvm/test/tools/llubi/noalias_callsite.ll
 create mode 100644 llvm/test/tools/llubi/noalias_callsite_indirect.ll
 create mode 100644 llvm/test/tools/llubi/noalias_callsite_intrinsic.ll
 create mode 100644 llvm/test/tools/llubi/noalias_escaped_pointer.ll
 create mode 100644 llvm/test/tools/llubi/noalias_free.ll
 create mode 100644 llvm/test/tools/llubi/noalias_histogram.ll
 create mode 100644 llvm/test/tools/llubi/noalias_libc_read.ll
 create mode 100644 llvm/test/tools/llubi/noalias_memcpy.ll
 create mode 100644 llvm/test/tools/llubi/noalias_memcpy_destination.ll
 create mode 100644 llvm/test/tools/llubi/noalias_memmove.ll
 create mode 100644 llvm/test/tools/llubi/noalias_memory_effects_valid.ll
 create mode 100644 llvm/test/tools/llubi/noalias_memset.ll
 create mode 100644 llvm/test/tools/llubi/noalias_memset_inline.ll
 create mode 100644 llvm/test/tools/llubi/noalias_outside_range_foreign_first.ll
 create mode 100644 llvm/test/tools/llubi/noalias_outside_range_unused.ll
 delete mode 100644 llvm/test/tools/llubi/noalias_prune_stale_node.ll
 create mode 100644 llvm/test/tools/llubi/noalias_returned_pointer.ll
 create mode 100644 llvm/test/tools/llubi/noalias_returned_pointer_foreign_access.ll
 create mode 100644 llvm/test/tools/llubi/noalias_wildcard.ll
 create mode 100644 llvm/test/tools/llubi/noalias_wildcard_foreign_first.ll
 create mode 100644 llvm/test/tools/llubi/noalias_wildcard_outside_allocation.ll
 create mode 100644 llvm/test/tools/llubi/noalias_wildcard_valid.ll

diff --git a/llvm/test/tools/llubi/noalias_after_return.ll b/llvm/test/tools/llubi/noalias_after_return.ll
deleted file mode 100644
index 0ad633eddf420d..00000000000000
--- a/llvm/test/tools/llubi/noalias_after_return.ll
+++ /dev/null
@@ -1,33 +0,0 @@
-; 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' in activation #1 based on raw/root
-; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
-; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
-; CHECK-NEXT:   store i32 1, ptr %x, align 4
-; CHECK-NEXT:   ret void
-; CHECK-NEXT: NoAlias: erased inactive protector node #1
-; CHECK-NEXT: NoAlias: ended activation #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_mut1.ll b/llvm/test/tools/llubi/noalias_aliasing_mut1.ll
new file mode 100644
index 00000000000000..6de7199ea2421e
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_aliasing_mut1.ll
@@ -0,0 +1,40 @@
+; 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
+
+; Adapted from Miri's tests/fail/both_borrows/aliasing_mut1.rs.
+; An indirect call with no call-site attributes must still create distinct
+; noalias tags for the callee's two parameters, even for identical arguments.
+; Unlike Rust reference retagging, noalias does not implicitly read either
+; argument: the first store succeeds, and the second store must fail.
+
+define void @write_both(ptr noalias %x, ptr noalias %y) {
+  store i32 1, ptr %x
+  store i32 2, ptr %y
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  store i32 0, ptr %a
+  %fn = select i1 true, ptr @write_both, ptr null
+  call void %fn(ptr %a, ptr %a)
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0xC [a]
+; CHECK-NEXT:   store i32 0, ptr %a, align 4
+; CHECK-NEXT:   %fn = select i1 true, ptr @write_both, ptr null => ptr 0x8 [@write_both]
+; CHECK-NEXT: Entering function: write_both
+; CHECK-NEXT:   ptr %x = ptr 0xC [a]
+; CHECK-NEXT:   ptr %y = ptr 0xC [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %x, align 4
+; CHECK-NEXT: NoAlias: noalias violation: write through node #2 on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i32 2, ptr %y, align 4 at @write_both <stdin>:12
+; CHECK-NEXT: #1   call void %fn(ptr %a, ptr %a) at @main <stdin>:20
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through node #2 on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_aliasing_mut2.ll b/llvm/test/tools/llubi/noalias_aliasing_mut2.ll
new file mode 100644
index 00000000000000..70adf8e88ffeae
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_aliasing_mut2.ll
@@ -0,0 +1,39 @@
+; 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
+
+; Adapted from Miri's tests/fail/both_borrows/aliasing_mut2.rs.
+; Both arguments carry only noalias. The indirect call has no call-site
+; attributes. The explicit read through one argument makes the later write
+; through its sibling UB.
+
+define void @read_write(ptr noalias %x, ptr noalias %y) {
+  %v = load i32, ptr %x
+  store i32 2, ptr %y
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  store i32 0, ptr %a
+  %fn = select i1 true, ptr @read_write, ptr null
+  call void %fn(ptr %a, ptr %a)
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0xC [a]
+; CHECK-NEXT:   store i32 0, ptr %a, align 4
+; CHECK-NEXT:   %fn = select i1 true, ptr @read_write, ptr null => ptr 0x8 [@read_write]
+; CHECK-NEXT: Entering function: read_write
+; CHECK-NEXT:   ptr %x = ptr 0xC [a]
+; CHECK-NEXT:   ptr %y = ptr 0xC [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 read through node #1 on 'a' bytes [0, 4): unaccessed -> reads by node #1
+; CHECK-NEXT: NoAlias: read through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   %v = load i32, ptr %x, align 4 => i32 0
+; CHECK-NEXT: NoAlias: noalias violation: write through node #2 on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i32 2, ptr %y, align 4 at @read_write <stdin>:11
+; CHECK-NEXT: #1   call void %fn(ptr %a, ptr %a) at @main <stdin>:19
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through node #2 on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_aliasing_reads.ll b/llvm/test/tools/llubi/noalias_aliasing_reads.ll
index ab80f0ade27b68..9c2f886f6999d5 100644
--- a/llvm/test/tools/llubi/noalias_aliasing_reads.ll
+++ b/llvm/test/tools/llubi/noalias_aliasing_reads.ll
@@ -29,8 +29,6 @@ define void @main() {
 ; CHECK-NEXT: NoAlias: read through node #2 on 'a' bytes [0, 4) checked 1 active noalias activation
 ; CHECK-NEXT:   %vy = load i32, ptr %y, align 4 => i32 42
 ; CHECK-NEXT:   ret void
-; CHECK-NEXT: NoAlias: erased inactive protector node #1
-; CHECK-NEXT: NoAlias: erased inactive protector node #2
 ; CHECK-NEXT: NoAlias: ended activation #1
 ; CHECK-NEXT: Exiting function: read_both
 ; CHECK-NEXT:   call void @read_both(ptr %a, ptr %a)
diff --git a/llvm/test/tools/llubi/noalias_aliasing_unused.ll b/llvm/test/tools/llubi/noalias_aliasing_unused.ll
new file mode 100644
index 00000000000000..a6174c9d5a8c10
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_aliasing_unused.ll
@@ -0,0 +1,60 @@
+; 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
+
+; Variants of Miri's tests/fail/both_borrows/aliasing_mut{1,2}.rs with one
+; argument unused. LLVM noalias does not perform Rust's implicit retag reads,
+; so identical arguments are allowed when only one is accessed, regardless
+; of argument order. Protector removal must also succeed.
+
+define void @write_first(ptr noalias %x, ptr noalias %y) {
+  store i32 1, ptr %x
+  ret void
+}
+
+define void @write_second(ptr noalias %x, ptr noalias %y) {
+  store i32 2, ptr %y
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  store i32 0, ptr %a
+  %first = select i1 true, ptr @write_first, ptr null
+  call void %first(ptr %a, ptr %a)
+  %second = select i1 true, ptr @write_second, ptr null
+  call void %second(ptr %a, ptr %a)
+  %v = load i32, ptr %a
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0xC [a]
+; CHECK-NEXT:   store i32 0, ptr %a, align 4
+; CHECK-NEXT:   %first = select i1 true, ptr @write_first, ptr null => ptr 0x8 [@write_first]
+; CHECK-NEXT: Entering function: write_first
+; CHECK-NEXT:   ptr %x = ptr 0xC [a]
+; CHECK-NEXT:   ptr %y = ptr 0xC [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %x, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended activation #1
+; CHECK-NEXT: Exiting function: write_first
+; CHECK-NEXT:   call void %first(ptr %a, ptr %a)
+; CHECK-NEXT:   %second = select i1 true, ptr @write_second, ptr null => ptr 0xA [@write_second]
+; CHECK-NEXT: Entering function: write_second
+; CHECK-NEXT:   ptr %x = ptr 0xC [a]
+; CHECK-NEXT:   ptr %y = ptr 0xC [a]
+; CHECK-NEXT: NoAlias: created protector node #3 for 'a' in activation #2 based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #4 for 'a' in activation #2 based on raw/root
+; CHECK-NEXT: NoAlias: activation #2 write through node #4 on 'a' bytes [0, 4): unaccessed -> access including a write by node #4
+; CHECK-NEXT: NoAlias: write through node #4 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 2, ptr %y, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended activation #2
+; CHECK-NEXT: Exiting function: write_second
+; CHECK-NEXT:   call void %second(ptr %a, ptr %a)
+; CHECK-NEXT:   %v = load i32, ptr %a, align 4 => i32 2
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_byval.ll b/llvm/test/tools/llubi/noalias_byval.ll
new file mode 100644
index 00000000000000..fadf0fe15c0806
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_byval.ll
@@ -0,0 +1,34 @@
+; 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
+
+; The implicit byval copy reads through the original argument.
+
+define void @sink(ptr byval(i32) %p) {
+  ret void
+}
+define void @outer(ptr noalias %p, ptr %raw) {
+  store i32 1, ptr %p
+  call void @sink(ptr byval(i32) %raw)
+  ret void
+}
+define void @main() {
+  %a = alloca i32
+  call void @outer(ptr %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: Entering function: outer
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %raw = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %p, align 4
+; CHECK-NEXT: NoAlias: noalias violation: read through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @sink(ptr byval(i32) %raw) at @outer <stdin>:11
+; CHECK-NEXT: #1   call void @outer(ptr %a, ptr %a) at @main <stdin>:16
+; CHECK-NEXT: Immediate UB detected: noalias violation: read through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_callsite.ll b/llvm/test/tools/llubi/noalias_callsite.ll
new file mode 100644
index 00000000000000..aeef510cb072c0
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_callsite.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
+
+; A call-site-only noalias guarantee applies to this invocation.
+
+define void @plain_write_read(ptr %p, ptr %q) {
+  store i32 1, ptr %p
+  %v = load i32, ptr %q
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  call void @plain_write_read(ptr noalias %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: Entering function: plain_write_read
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %q = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %p, align 4
+; CHECK-NEXT: NoAlias: noalias violation: read through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %v = load i32, ptr %q, align 4 at @plain_write_read <stdin>:8
+; CHECK-NEXT: #1   call void @plain_write_read(ptr noalias %a, ptr %a) at @main <stdin>:14
+; CHECK-NEXT: Immediate UB detected: noalias violation: read through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_callsite_indirect.ll b/llvm/test/tools/llubi/noalias_callsite_indirect.ll
new file mode 100644
index 00000000000000..bf367e7ec1c0ee
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_callsite_indirect.ll
@@ -0,0 +1,34 @@
+; 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
+
+; A call-site-only noalias guarantee applies to this invocation.
+
+define void @plain_write_read(ptr %p, ptr %q) {
+  store i32 1, ptr %p
+  %v = load i32, ptr %q
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  %fn = select i1 true, ptr @plain_write_read, ptr null
+  call void %fn(ptr noalias %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0xC [a]
+; CHECK-NEXT:   %fn = select i1 true, ptr @plain_write_read, ptr null => ptr 0x8 [@plain_write_read]
+; CHECK-NEXT: Entering function: plain_write_read
+; CHECK-NEXT:   ptr %p = ptr 0xC [a]
+; CHECK-NEXT:   ptr %q = ptr 0xC [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %p, align 4
+; CHECK-NEXT: NoAlias: noalias violation: read through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %v = load i32, ptr %q, align 4 at @plain_write_read <stdin>:8
+; CHECK-NEXT: #1   call void %fn(ptr noalias %a, ptr %a) at @main <stdin>:15
+; CHECK-NEXT: Immediate UB detected: noalias violation: read through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_callsite_intrinsic.ll b/llvm/test/tools/llubi/noalias_callsite_intrinsic.ll
new file mode 100644
index 00000000000000..11f8ab9c7e4e7d
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_callsite_intrinsic.ll
@@ -0,0 +1,25 @@
+; 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
+
+; Call-site noalias also applies to calls without an interpreter frame.
+
+declare void @llvm.memmove.p0.p0.i64(ptr, ptr, i64, i1)
+define void @main() {
+  %a = alloca i32
+  store i32 0, ptr %a
+  call void @llvm.memmove.p0.p0.i64(ptr noalias %a, ptr %a, i64 4, i1 false)
+  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: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 read through raw/root on 'a' bytes [0, 4): unaccessed -> reads by raw/root
+; CHECK-NEXT: NoAlias: read through raw/root on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT: NoAlias: noalias violation: write through node #1 on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @llvm.memmove.p0.p0.i64(ptr noalias %a, ptr %a, i64 4, i1 false) at @main <stdin>:10
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through node #1 on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: NoAlias: ended activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_disjoint_intervals.ll b/llvm/test/tools/llubi/noalias_disjoint_intervals.ll
index e328b7919e1869..cd2e1fbb2377bc 100644
--- a/llvm/test/tools/llubi/noalias_disjoint_intervals.ll
+++ b/llvm/test/tools/llubi/noalias_disjoint_intervals.ll
@@ -31,8 +31,6 @@ define void @main() {
 ; CHECK-NEXT: NoAlias: write through node #2 on 'a' bytes [4, 8) checked 1 active noalias activation
 ; CHECK-NEXT:   store i32 2, ptr %y, align 4
 ; CHECK-NEXT:   ret void
-; CHECK-NEXT: NoAlias: erased inactive protector node #1
-; CHECK-NEXT: NoAlias: erased inactive protector node #2
 ; CHECK-NEXT: NoAlias: ended activation #1
 ; CHECK-NEXT: Exiting function: write_disjoint
 ; CHECK-NEXT:   call void @write_disjoint(ptr %x, ptr %y)
diff --git a/llvm/test/tools/llubi/noalias_escaped_pointer.ll b/llvm/test/tools/llubi/noalias_escaped_pointer.ll
new file mode 100644
index 00000000000000..5b2e10a8401c89
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_escaped_pointer.ll
@@ -0,0 +1,84 @@
+; 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
+
+; Storing and reloading an inner noalias pointer preserves outer ancestry.
+
+define void @capture(ptr noalias %p, ptr %slot) {
+  store ptr %p, ptr %slot
+  ret void
+}
+
+define void @write_one(ptr noalias %p) {
+  store i32 3, ptr %p
+  ret void
+}
+
+define void @escaped_outer(ptr noalias %p) {
+  %slot = alloca ptr
+  call void @capture(ptr %p, ptr %slot)
+  %q = load ptr, ptr %slot
+  store i32 1, ptr %p
+  store i32 2, ptr %q
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  call void @escaped_outer(ptr %a)
+  ; Retagging an escaped pointer also works after all protectors have ended.
+  %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: Entering function: escaped_outer
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT:   %slot = alloca ptr, align 8 => ptr 0x10 [slot]
+; CHECK-NEXT: Entering function: capture
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %slot = ptr 0x10 [slot]
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' in activation #2 based on node #1
+; CHECK-NEXT:   store ptr %p, ptr %slot, align 8
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended activation #2
+; CHECK-NEXT: Exiting function: capture
+; CHECK-NEXT:   call void @capture(ptr %p, ptr %slot)
+; CHECK-NEXT:   %q = load ptr, ptr %slot, align 8 => ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %p, align 4
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): access including a write by node #1 -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #2 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 2, ptr %q, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended activation #1
+; CHECK-NEXT: Exiting function: escaped_outer
+; CHECK-NEXT:   call void @escaped_outer(ptr %a)
+; CHECK-NEXT:   %slot = alloca ptr, align 8 => ptr 0x20 [slot]
+; CHECK-NEXT: Entering function: capture
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %slot = ptr 0x20 [slot]
+; CHECK-NEXT: NoAlias: created protector node #3 for 'a' in activation #3 based on raw/root
+; CHECK-NEXT:   store ptr %p, ptr %slot, align 8
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended activation #3
+; 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 %p = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #4 for 'a' in activation #4 based on raw/root
+; CHECK-NEXT: NoAlias: activation #4 write through node #4 on 'a' bytes [0, 4): unaccessed -> access including a write by node #4
+; CHECK-NEXT: NoAlias: write through node #4 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 3, ptr %p, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended activation #4
+; 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_free.ll b/llvm/test/tools/llubi/noalias_free.ll
new file mode 100644
index 00000000000000..06b1084de54020
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_free.ll
@@ -0,0 +1,35 @@
+; 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
+
+; Deallocation must check active protectors before dropping allocation state.
+
+declare ptr @malloc(i64)
+declare void @free(ptr)
+
+define void @free_bad(ptr noalias %p, ptr %raw) {
+  store i32 1, ptr %p
+  call void @free(ptr %raw)
+  ret void
+}
+
+define void @main() {
+  %a = call ptr @malloc(i64 4)
+  call void @free_bad(ptr %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = call ptr @malloc(i64 4) => ptr 0x10 [a]
+; CHECK-NEXT: Entering function: free_bad
+; CHECK-NEXT:   ptr %p = ptr 0x10 [a]
+; CHECK-NEXT:   ptr %raw = ptr 0x10 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %p, align 4
+; CHECK-NEXT: NoAlias: noalias violation: deallocation through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @free(ptr %raw) at @free_bad <stdin>:11
+; CHECK-NEXT: #1   call void @free_bad(ptr %a, ptr %a) at @main <stdin>:17
+; CHECK-NEXT: Immediate UB detected: noalias violation: deallocation through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_histogram.ll b/llvm/test/tools/llubi/noalias_histogram.ll
new file mode 100644
index 00000000000000..d426b13bc8f1a6
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_histogram.ll
@@ -0,0 +1,33 @@
+; 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
+
+; Active histogram lanes are writes for noalias checking.
+
+define void @outer(ptr noalias %p, ptr %raw) {
+  store i32 0, ptr %p
+  %v = insertelement <2 x ptr> poison, ptr %raw, i32 0
+  call void @llvm.experimental.vector.histogram.add.v2p0.i32(<2 x ptr> %v, i32 1, <2 x i1> <i1 true, i1 false>)
+  ret void
+}
+define void @main() {
+  %a = alloca i32
+  call void @outer(ptr %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: Entering function: outer
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %raw = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 0, ptr %p, align 4
+; CHECK-NEXT:   %v = insertelement <2 x ptr> poison, ptr %raw, i32 0 => { ptr 0x8 [a], poison }
+; CHECK-NEXT: NoAlias: noalias violation: write through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @llvm.experimental.vector.histogram.add.v2p0.i32(<2 x ptr> %v, i32 1, <2 x i1> <i1 true, i1 false>) at @outer <stdin>:9
+; CHECK-NEXT: #1   call void @outer(ptr %a, ptr %a) at @main <stdin>:14
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_libc_read.ll b/llvm/test/tools/llubi/noalias_libc_read.ll
new file mode 100644
index 00000000000000..e84fb4aa159540
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_libc_read.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
+
+; Even reading the terminating null byte is a foreign read.
+
+declare i32 @puts(ptr)
+define void @outer(ptr noalias %p, ptr %raw) {
+  store i8 0, ptr %p
+  %r = call i32 @puts(ptr %raw)
+  ret void
+}
+define void @main() {
+  %a = alloca i8
+  call void @outer(ptr %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i8, align 1 => ptr 0x8 [a]
+; CHECK-NEXT: Entering function: outer
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %raw = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 1): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 1) checked 1 active noalias activation
+; CHECK-NEXT:   store i8 0, ptr %p, align 1
+; CHECK-NEXT: NoAlias: noalias violation: read through raw/root on 'a' bytes [0, 1) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %r = call i32 @puts(ptr %raw) at @outer <stdin>:9
+; CHECK-NEXT: #1   call void @outer(ptr %a, ptr %a) at @main <stdin>:14
+; CHECK-NEXT: Immediate UB detected: noalias violation: read through raw/root on 'a' bytes [0, 1) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_memcpy.ll b/llvm/test/tools/llubi/noalias_memcpy.ll
new file mode 100644
index 00000000000000..1343b2051bb68d
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_memcpy.ll
@@ -0,0 +1,39 @@
+; 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
+
+; A foreign memcpy source is a read; the destination is a distinct allocation.
+
+declare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1)
+
+define void @memcpy_bad(ptr noalias %p, ptr %raw) {
+  %dst = alloca i32
+  store i32 1, ptr %p
+  call void @llvm.memcpy.p0.p0.i64(ptr %dst, ptr %raw, i64 4, i1 false)
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  call void @memcpy_bad(ptr %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: Entering function: memcpy_bad
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %raw = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT:   %dst = alloca i32, align 4 => ptr 0x10 [dst]
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %p, align 4
+; CHECK-NEXT: NoAlias: created protector node #2 for 'dst' in activation #2 based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #3 for 'a' in activation #2 based on raw/root
+; CHECK-NEXT: NoAlias: noalias violation: read through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @llvm.memcpy.p0.p0.i64(ptr %dst, ptr %raw, i64 4, i1 false) at @memcpy_bad <stdin>:11
+; CHECK-NEXT: #1   call void @memcpy_bad(ptr %a, ptr %a) at @main <stdin>:17
+; CHECK-NEXT: Immediate UB detected: noalias violation: read through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: NoAlias: ended activation #2
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_memcpy_destination.ll b/llvm/test/tools/llubi/noalias_memcpy_destination.ll
new file mode 100644
index 00000000000000..b33af58168adc4
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_memcpy_destination.ll
@@ -0,0 +1,43 @@
+; 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
+
+; A foreign memcpy destination must be recorded as a write.
+
+declare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1)
+
+define void @memcpy_bad(ptr noalias %p, ptr %raw) {
+  %src = alloca i32
+  store i32 0, ptr %src
+  store i32 1, ptr %p
+  call void @llvm.memcpy.p0.p0.i64(ptr %raw, ptr %src, i64 4, i1 false)
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  call void @memcpy_bad(ptr %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: Entering function: memcpy_bad
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %raw = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT:   %src = alloca i32, align 4 => ptr 0x10 [src]
+; CHECK-NEXT:   store i32 0, ptr %src, align 4
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %p, align 4
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' in activation #2 based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #3 for 'src' in activation #2 based on raw/root
+; CHECK-NEXT: NoAlias: activation #2 read through node #3 on 'src' bytes [0, 4): unaccessed -> reads by node #3
+; CHECK-NEXT: NoAlias: read through node #3 on 'src' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT: NoAlias: noalias violation: write through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @llvm.memcpy.p0.p0.i64(ptr %raw, ptr %src, i64 4, i1 false) at @memcpy_bad <stdin>:12
+; CHECK-NEXT: #1   call void @memcpy_bad(ptr %a, ptr %a) at @main <stdin>:18
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: NoAlias: ended activation #2
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_memmove.ll b/llvm/test/tools/llubi/noalias_memmove.ll
new file mode 100644
index 00000000000000..5de62e21925253
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_memmove.ll
@@ -0,0 +1,36 @@
+; 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
+
+; A foreign memmove source is a read; the destination is a distinct allocation.
+
+declare void @llvm.memmove.p0.p0.i64(ptr, ptr, i64, i1)
+
+define void @memmove_bad(ptr noalias %p, ptr %raw) {
+  %dst = alloca i32
+  store i32 1, ptr %p
+  call void @llvm.memmove.p0.p0.i64(ptr %dst, ptr %raw, i64 4, i1 false)
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  call void @memmove_bad(ptr %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: Entering function: memmove_bad
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %raw = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT:   %dst = alloca i32, align 4 => ptr 0x10 [dst]
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %p, align 4
+; CHECK-NEXT: NoAlias: noalias violation: read through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @llvm.memmove.p0.p0.i64(ptr %dst, ptr %raw, i64 4, i1 false) at @memmove_bad <stdin>:11
+; CHECK-NEXT: #1   call void @memmove_bad(ptr %a, ptr %a) at @main <stdin>:17
+; CHECK-NEXT: Immediate UB detected: noalias violation: read through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_memory_effects_valid.ll b/llvm/test/tools/llubi/noalias_memory_effects_valid.ll
new file mode 100644
index 00000000000000..9d40806eda9616
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_memory_effects_valid.ll
@@ -0,0 +1,131 @@
+; 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
+
+; Zero-length and masked-off accesses do not conflict; local effects remain valid.
+
+declare void @llvm.memset.p0.i64(ptr, i8, i64, i1)
+declare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1)
+declare void @llvm.memmove.p0.p0.i64(ptr, ptr, i64, i1)
+declare ptr @malloc(i64)
+declare void @free(ptr)
+declare i32 @puts(ptr)
+
+define void @outer(ptr noalias %p, ptr %raw) {
+  store i32 0, ptr %p
+  call void @llvm.memset.p0.i64(ptr %raw, i8 1, i64 0, i1 false)
+  call void @llvm.memcpy.p0.p0.i64(ptr %raw, ptr %raw, i64 0, i1 false)
+  %v = insertelement <2 x ptr> poison, ptr %raw, i32 0
+  %ptrs = insertelement <2 x ptr> %v, ptr %p, i32 1
+  call void @llvm.experimental.vector.histogram.add.v2p0.i32(<2 x ptr> %ptrs, i32 1, <2 x i1> <i1 false, i1 true>)
+  %dst = alloca i32
+  call void @llvm.memcpy.p0.p0.i64(ptr %dst, ptr %p, i64 4, i1 false)
+  call void @llvm.memmove.p0.p0.i64(ptr %p, ptr %p, i64 4, i1 false)
+  call void @llvm.memset.p0.i64(ptr %p, i8 65, i64 1, i1 false)
+  %r = call i32 @puts(ptr %p)
+  call void @free(ptr %p)
+  ret void
+}
+define void @unused(ptr noalias %p) {
+  ret void
+}
+define void @foreign_free(ptr noalias %unused, ptr %raw) {
+  call void @free(ptr %raw)
+  ret void
+}
+define void @main() {
+  %a = call ptr @malloc(i64 4)
+  call void @outer(ptr %a, ptr %a)
+  %b = call ptr @malloc(i64 4)
+  call void @foreign_free(ptr %b, ptr %b)
+  call void @unused(ptr null)
+  call void @unused(ptr poison)
+  %bits = ptrtoint ptr @unused to i64
+  %wild = inttoptr i64 4096 to ptr
+  call void @unused(ptr %wild)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = call ptr @malloc(i64 4) => ptr 0x10 [a]
+; CHECK-NEXT: Entering function: outer
+; CHECK-NEXT:   ptr %p = ptr 0x10 [a]
+; CHECK-NEXT:   ptr %raw = ptr 0x10 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 0, ptr %p, align 4
+; CHECK-NEXT:   call void @llvm.memset.p0.i64(ptr %raw, i8 1, i64 0, i1 false)
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' in activation #2 based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #3 for 'a' in activation #2 based on raw/root
+; CHECK-NEXT: NoAlias: ended activation #2
+; CHECK-NEXT:   call void @llvm.memcpy.p0.p0.i64(ptr %raw, ptr %raw, i64 0, i1 false)
+; CHECK-NEXT:   %v = insertelement <2 x ptr> poison, ptr %raw, i32 0 => { ptr 0x10 [a], poison }
+; CHECK-NEXT:   %ptrs = insertelement <2 x ptr> %v, ptr %p, i32 1 => { ptr 0x10 [a], ptr 0x10 [a] }
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): access including a write by node #1 -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   call void @llvm.experimental.vector.histogram.add.v2p0.i32(<2 x ptr> %ptrs, i32 1, <2 x i1> <i1 false, i1 true>)
+; CHECK-NEXT:   %dst = alloca i32, align 4 => ptr 0x18 [dst]
+; CHECK-NEXT: NoAlias: created protector node #4 for 'dst' in activation #3 based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #5 for 'a' in activation #3 based on node #1
+; CHECK-NEXT: NoAlias: activation #1 read through node #1 on 'a' bytes [0, 4): access including a write by node #1 -> access including a write by node #1
+; CHECK-NEXT: NoAlias: activation #3 read through node #5 on 'a' bytes [0, 4): unaccessed -> reads by node #5
+; CHECK-NEXT: NoAlias: read through node #5 on 'a' bytes [0, 4) checked 2 active noalias activations
+; CHECK-NEXT: NoAlias: activation #3 write through node #4 on 'dst' bytes [0, 4): unaccessed -> access including a write by node #4
+; CHECK-NEXT: NoAlias: write through node #4 on 'dst' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT: NoAlias: ended activation #3
+; CHECK-NEXT:   call void @llvm.memcpy.p0.p0.i64(ptr %dst, ptr %p, i64 4, i1 false)
+; CHECK-NEXT: NoAlias: activation #1 read through node #1 on 'a' bytes [0, 4): access including a write by node #1 -> access including a write by node #1
+; CHECK-NEXT: NoAlias: read through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): access including a write by node #1 -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   call void @llvm.memmove.p0.p0.i64(ptr %p, ptr %p, i64 4, i1 false)
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 1): access including a write by node #1 -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 1) checked 1 active noalias activation
+; CHECK-NEXT:   call void @llvm.memset.p0.i64(ptr %p, i8 65, i64 1, i1 false)
+; CHECK-NEXT: NoAlias: activation #1 read through node #1 on 'a' bytes [0, 1): access including a write by node #1 -> access including a write by node #1
+; CHECK-NEXT: NoAlias: read through node #1 on 'a' bytes [0, 1) checked 1 active noalias activation
+; CHECK-NEXT: NoAlias: activation #1 read through node #1 on 'a' bytes [1, 2): access including a write by node #1 -> access including a write by node #1
+; CHECK-NEXT: NoAlias: read through node #1 on 'a' bytes [1, 2) checked 1 active noalias activation
+; CHECK-NEXT: A
+; CHECK-NEXT:   %r = call i32 @puts(ptr %p) => i32 1
+; CHECK-NEXT: NoAlias: activation #1 deallocation through node #1 on 'a' bytes [0, 4): access including a write by node #1 -> access including a write by node #1
+; CHECK-NEXT: NoAlias: deallocation through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   call void @free(ptr %p)
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended activation #1
+; CHECK-NEXT: Exiting function: outer
+; CHECK-NEXT:   call void @outer(ptr %a, ptr %a)
+; CHECK-NEXT:   %b = call ptr @malloc(i64 4) => ptr 0x20 [b]
+; CHECK-NEXT: Entering function: foreign_free
+; CHECK-NEXT:   ptr %unused = ptr 0x20 [b]
+; CHECK-NEXT:   ptr %raw = ptr 0x20 [b]
+; CHECK-NEXT: NoAlias: created protector node #6 for 'b' in activation #4 based on raw/root
+; CHECK-NEXT: NoAlias: activation #4 deallocation through raw/root on 'b' bytes [0, 4): unaccessed -> access including a write by raw/root
+; CHECK-NEXT: NoAlias: deallocation through raw/root on 'b' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   call void @free(ptr %raw)
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended activation #4
+; CHECK-NEXT: Exiting function: foreign_free
+; CHECK-NEXT:   call void @foreign_free(ptr %b, ptr %b)
+; CHECK-NEXT: Entering function: unused
+; CHECK-NEXT:   ptr %p = ptr 0x0 [nullary]
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended activation #5
+; CHECK-NEXT: Exiting function: unused
+; CHECK-NEXT:   call void @unused(ptr null)
+; CHECK-NEXT: Entering function: unused
+; CHECK-NEXT:   ptr %p = poison
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: unused
+; CHECK-NEXT:   call void @unused(ptr poison)
+; CHECK-NEXT:   %bits = ptrtoint ptr @unused to i64 => i64 8
+; CHECK-NEXT:   %wild = inttoptr i64 4096 to ptr => ptr 0x1000 [wildcard]
+; CHECK-NEXT: Entering function: unused
+; CHECK-NEXT:   ptr %p = ptr 0x1000 [wildcard]
+; CHECK-NEXT: NoAlias: created protector node #7 for wildcard provenance in activation #6 based on raw/root
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended activation #6
+; CHECK-NEXT: Exiting function: unused
+; CHECK-NEXT:   call void @unused(ptr %wild)
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_memset.ll b/llvm/test/tools/llubi/noalias_memset.ll
new file mode 100644
index 00000000000000..0a855204bf94e8
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_memset.ll
@@ -0,0 +1,34 @@
+; 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
+
+; A foreign memset is a write, even though it does not use ExecutorBase::store.
+
+declare void @llvm.memset.p0.i64(ptr, i8, i64, i1)
+
+define void @memset_bad(ptr noalias %p, ptr %raw) {
+  store i32 1, ptr %p
+  call void @llvm.memset.p0.i64(ptr %raw, i8 0, i64 4, i1 false)
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  call void @memset_bad(ptr %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: Entering function: memset_bad
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %raw = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %p, align 4
+; CHECK-NEXT: NoAlias: noalias violation: write through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @llvm.memset.p0.i64(ptr %raw, i8 0, i64 4, i1 false) at @memset_bad <stdin>:10
+; CHECK-NEXT: #1   call void @memset_bad(ptr %a, ptr %a) at @main <stdin>:16
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_memset_inline.ll b/llvm/test/tools/llubi/noalias_memset_inline.ll
new file mode 100644
index 00000000000000..47cf03e81ef4e3
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_memset_inline.ll
@@ -0,0 +1,34 @@
+; 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
+
+; A foreign memset is a write, even though it does not use ExecutorBase::store.
+
+declare void @llvm.memset.inline.p0.i64(ptr, i8, i64, i1)
+
+define void @memset_bad(ptr noalias %p, ptr %raw) {
+  store i32 1, ptr %p
+  call void @llvm.memset.inline.p0.i64(ptr %raw, i8 0, i64 4, i1 false)
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  call void @memset_bad(ptr %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: Entering function: memset_bad
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %raw = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %p, align 4
+; CHECK-NEXT: NoAlias: noalias violation: write through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @llvm.memset.inline.p0.i64(ptr %raw, i8 0, i64 4, i1 false) at @memset_bad <stdin>:10
+; CHECK-NEXT: #1   call void @memset_bad(ptr %a, ptr %a) at @main <stdin>:16
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_nested_activations.ll b/llvm/test/tools/llubi/noalias_nested_activations.ll
index 1cd8cff29b6737..985ba9ea8bf34b 100644
--- a/llvm/test/tools/llubi/noalias_nested_activations.ll
+++ b/llvm/test/tools/llubi/noalias_nested_activations.ll
@@ -42,7 +42,6 @@ define void @main() {
 ; CHECK-NEXT: NoAlias: write through node #2 on 'a' bytes [0, 4) checked 2 active noalias activations
 ; CHECK-NEXT:   store i32 1, ptr %x, align 4
 ; CHECK-NEXT:   ret void
-; CHECK-NEXT: NoAlias: erased inactive protector node #2
 ; CHECK-NEXT: NoAlias: ended activation #2
 ; CHECK-NEXT: Exiting function: write_inner
 ; CHECK-NEXT:   call void @write_inner(ptr %x)
@@ -50,7 +49,6 @@ define void @main() {
 ; CHECK-NEXT: NoAlias: read through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
 ; CHECK-NEXT:   %v = load i32, ptr %x, align 4 => i32 1
 ; CHECK-NEXT:   ret void
-; CHECK-NEXT: NoAlias: erased inactive protector node #1
 ; CHECK-NEXT: NoAlias: ended activation #1
 ; CHECK-NEXT: Exiting function: outer_ok
 ; CHECK-NEXT:   call void @outer_ok(ptr %a)
diff --git a/llvm/test/tools/llubi/noalias_outside_range.ll b/llvm/test/tools/llubi/noalias_outside_range.ll
index 0db9d92ad235da..a1c36c10c0595c 100644
--- a/llvm/test/tools/llubi/noalias_outside_range.ll
+++ b/llvm/test/tools/llubi/noalias_outside_range.ll
@@ -19,6 +19,7 @@ define void @stuff(ptr noalias %x, ptr %raw) {
 
 define void @main() {
   %data = alloca [4 x i8]
+  store [4 x i8] [i8 0, i8 1, i8 2, i8 3], ptr %data
   %raw = getelementptr [4 x i8], ptr %data, i64 0, i64 0
   call void @stuff(ptr %raw, ptr %raw)
   ret void
@@ -26,6 +27,7 @@ define void @main() {
 
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %data = alloca [4 x i8], align 1 => ptr 0x8 [data]
+; CHECK-NEXT:   store [4 x i8] c"\00\01\02\03", ptr %data, align 1
 ; 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]
@@ -38,15 +40,15 @@ define void @main() {
 ; CHECK-NEXT:   %x2 = getelementptr i8, ptr %x, i64 2 => ptr 0xA [data + 2]
 ; CHECK-NEXT: NoAlias: activation #1 read through node #1 on 'data' bytes [2, 3): unaccessed -> reads by node #1
 ; CHECK-NEXT: NoAlias: read through node #1 on 'data' bytes [2, 3) checked 1 active noalias activation
-; CHECK-NEXT:   %v2 = load i8, ptr %x2, align 1 => i8 62
+; CHECK-NEXT:   %v2 = load i8, ptr %x2, align 1 => i8 2
 ; CHECK-NEXT:   %x3 = getelementptr i8, ptr %x, i64 3 => ptr 0xB [data + 3]
 ; CHECK-NEXT: NoAlias: activation #1 read through node #1 on 'data' bytes [3, 4): unaccessed -> reads by node #1
 ; CHECK-NEXT: NoAlias: read through node #1 on 'data' bytes [3, 4) checked 1 active noalias activation
-; CHECK-NEXT:   %v3 = load i8, ptr %x3, align 1 => i8 -117
+; CHECK-NEXT:   %v3 = load i8, ptr %x3, align 1 => i8 3
 ; CHECK-NEXT:   %raw3 = getelementptr i8, ptr %raw, i64 3 => ptr 0xB [data + 3]
 ; CHECK-NEXT: NoAlias: noalias violation: write through raw/root on 'data' bytes [3, 4) combines multiple access classes with a write in activation #1
 ; CHECK-NEXT: Stacktrace:
 ; CHECK-NEXT: #0   store i8 42, ptr %raw3, align 1 at @stuff <stdin>:16
-; CHECK-NEXT: #1   call void @stuff(ptr %raw, ptr %raw) at @main <stdin>:23
+; CHECK-NEXT: #1   call void @stuff(ptr %raw, ptr %raw) at @main <stdin>:24
 ; CHECK-NEXT: Immediate UB detected: noalias violation: write through raw/root on 'data' bytes [3, 4) combines multiple access classes with a write in activation #1
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_outside_range_foreign_first.ll b/llvm/test/tools/llubi/noalias_outside_range_foreign_first.ll
new file mode 100644
index 00000000000000..477fbf9784507f
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_outside_range_foreign_first.ll
@@ -0,0 +1,40 @@
+; 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
+
+; Variant of Miri's tests/fail/tree_borrows/outside-range.rs with the access
+; order reversed. The foreign write to byte 1 is initially allowed, but the
+; subsequent local read of that byte must fail. Access history must include
+; foreign writes even before the first local access to the affected bytes.
+
+define void @foreign_first(ptr noalias %x, ptr %raw) {
+  %raw1 = getelementptr i8, ptr %raw, i64 1
+  store i8 42, ptr %raw1
+  %x1 = getelementptr i8, ptr %x, i64 1
+  %v = load i8, ptr %x1
+  ret void
+}
+
+define void @main() {
+  %data = alloca [4 x i8]
+  store [4 x i8] [i8 0, i8 1, i8 2, i8 3], ptr %data
+  call void @foreign_first(ptr %data, ptr %data)
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %data = alloca [4 x i8], align 1 => ptr 0x8 [data]
+; CHECK-NEXT:   store [4 x i8] c"\00\01\02\03", ptr %data, align 1
+; CHECK-NEXT: Entering function: foreign_first
+; CHECK-NEXT:   ptr %x = ptr 0x8 [data]
+; CHECK-NEXT:   ptr %raw = ptr 0x8 [data]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'data' in activation #1 based on raw/root
+; CHECK-NEXT:   %raw1 = getelementptr i8, ptr %raw, i64 1 => ptr 0x9 [data + 1]
+; CHECK-NEXT: NoAlias: activation #1 write through raw/root on 'data' bytes [1, 2): unaccessed -> access including a write by raw/root
+; CHECK-NEXT: NoAlias: write through raw/root on 'data' bytes [1, 2) checked 1 active noalias activation
+; CHECK-NEXT:   store i8 42, ptr %raw1, align 1
+; CHECK-NEXT:   %x1 = getelementptr i8, ptr %x, i64 1 => ptr 0x9 [data + 1]
+; CHECK-NEXT: NoAlias: noalias violation: read through node #1 on 'data' bytes [1, 2) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %v = load i8, ptr %x1, align 1 at @foreign_first <stdin>:13
+; CHECK-NEXT: #1   call void @foreign_first(ptr %data, ptr %data) at @main <stdin>:20
+; CHECK-NEXT: Immediate UB detected: noalias violation: read through node #1 on 'data' bytes [1, 2) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_outside_range_unused.ll b/llvm/test/tools/llubi/noalias_outside_range_unused.ll
new file mode 100644
index 00000000000000..76a19806a72a1c
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_outside_range_unused.ll
@@ -0,0 +1,50 @@
+; 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
+
+; Passing variant of Miri's tests/fail/tree_borrows/outside-range.rs.
+; Foreign writes to bytes never accessed through x are allowed, both before
+; and after local accesses to other bytes. This remains valid on return.
+
+define void @disjoint(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
+  %raw3 = getelementptr i8, ptr %raw, i64 3
+  store i8 42, ptr %raw3
+  ret void
+}
+
+define void @main() {
+  %data = alloca [4 x i8]
+  store [4 x i8] [i8 0, i8 1, i8 2, i8 3], ptr %data
+  call void @disjoint(ptr %data, ptr %data)
+  %v = load [4 x i8], ptr %data
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %data = alloca [4 x i8], align 1 => ptr 0x8 [data]
+; CHECK-NEXT:   store [4 x i8] c"\00\01\02\03", ptr %data, align 1
+; CHECK-NEXT: Entering function: disjoint
+; CHECK-NEXT:   ptr %x = ptr 0x8 [data]
+; CHECK-NEXT:   ptr %raw = ptr 0x8 [data]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'data' in activation #1 based on raw/root
+; CHECK-NEXT:   %raw1 = getelementptr i8, ptr %raw, i64 1 => ptr 0x9 [data + 1]
+; CHECK-NEXT: NoAlias: activation #1 write through raw/root on 'data' bytes [1, 2): unaccessed -> access including a write by raw/root
+; CHECK-NEXT: NoAlias: write through raw/root on 'data' bytes [1, 2) checked 1 active noalias activation
+; 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: activation #1 read through node #1 on 'data' bytes [2, 3): unaccessed -> reads by node #1
+; CHECK-NEXT: NoAlias: read through node #1 on 'data' bytes [2, 3) checked 1 active noalias activation
+; CHECK-NEXT:   %v2 = load i8, ptr %x2, align 1 => i8 2
+; CHECK-NEXT:   %raw3 = getelementptr i8, ptr %raw, i64 3 => ptr 0xB [data + 3]
+; CHECK-NEXT: NoAlias: activation #1 write through raw/root on 'data' bytes [3, 4): unaccessed -> access including a write by raw/root
+; CHECK-NEXT: NoAlias: write through raw/root on 'data' bytes [3, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i8 42, ptr %raw3, align 1
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended activation #1
+; CHECK-NEXT: Exiting function: disjoint
+; CHECK-NEXT:   call void @disjoint(ptr %data, ptr %data)
+; CHECK-NEXT:   %v = load [4 x i8], ptr %data, align 1 => { i8 0, i8 42, i8 2, i8 42 }
+; 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
deleted file mode 100644
index 1d91bd9022b90c..00000000000000
--- a/llvm/test/tools/llubi/noalias_prune_stale_node.ll
+++ /dev/null
@@ -1,49 +0,0 @@
-; 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' in activation #1 based on raw/root
-; CHECK-NEXT:   store ptr %x, ptr %slot, align 8
-; CHECK-NEXT:   ret void
-; CHECK-NEXT: NoAlias: erased inactive protector node #1
-; CHECK-NEXT: NoAlias: ended activation #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' in activation #2 based on raw/root
-; CHECK-NEXT: NoAlias: activation #2 write through node #2 on 'a' bytes [0, 4): unaccessed -> access including a write by node #2
-; CHECK-NEXT: NoAlias: write through node #2 on 'a' bytes [0, 4) checked 1 active noalias activation
-; CHECK-NEXT:   store i32 1, ptr %x, align 4
-; CHECK-NEXT:   ret void
-; CHECK-NEXT: NoAlias: erased inactive protector node #2
-; CHECK-NEXT: NoAlias: ended activation #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_returned_pointer.ll b/llvm/test/tools/llubi/noalias_returned_pointer.ll
new file mode 100644
index 00000000000000..ebf39ac30e27a5
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_returned_pointer.ll
@@ -0,0 +1,64 @@
+; 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
+
+; A returned noalias pointer remains based on the still-active outer argument.
+
+define ptr @identity(ptr noalias %p) {
+  ret ptr %p
+}
+
+define void @returned_outer(ptr noalias %p) {
+  store i32 1, ptr %p
+  %q = call ptr @identity(ptr %p)
+  store i32 2, ptr %q
+  ; Retagging a returned pointer must also retain the inactive intermediate node.
+  %r = call ptr @identity(ptr %q)
+  store i32 3, ptr %r
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  call void @returned_outer(ptr %a)
+  ; The root pointer is usable again after all protectors have ended.
+  %v = load i32, ptr %a
+  store i32 4, ptr %a
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: Entering function: returned_outer
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %p, align 4
+; CHECK-NEXT: Entering function: identity
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' in activation #2 based on node #1
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: NoAlias: ended activation #2
+; CHECK-NEXT: Exiting function: identity
+; CHECK-NEXT:   %q = call ptr @identity(ptr %p) => ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): access including a write by node #1 -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #2 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 2, ptr %q, align 4
+; CHECK-NEXT: Entering function: identity
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #3 for 'a' in activation #3 based on node #2
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: NoAlias: ended activation #3
+; CHECK-NEXT: Exiting function: identity
+; CHECK-NEXT:   %r = call ptr @identity(ptr %q) => ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): access including a write by node #1 -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #3 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 3, ptr %r, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended activation #1
+; CHECK-NEXT: Exiting function: returned_outer
+; CHECK-NEXT:   call void @returned_outer(ptr %a)
+; CHECK-NEXT:   %v = load i32, ptr %a, align 4 => i32 3
+; CHECK-NEXT:   store i32 4, ptr %a, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_returned_pointer_foreign_access.ll b/llvm/test/tools/llubi/noalias_returned_pointer_foreign_access.ll
new file mode 100644
index 00000000000000..12686bf4eb4341
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_returned_pointer_foreign_access.ll
@@ -0,0 +1,44 @@
+; 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
+
+; After an inner return, its pointer still conflicts with an outer foreign alias.
+
+define ptr @identity(ptr noalias %p) {
+  ret ptr %p
+}
+
+define void @escaped_bad_outer(ptr noalias %p, ptr %raw) {
+  %q = call ptr @identity(ptr %p)
+  store i32 1, ptr %q
+  store i32 2, ptr %raw
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  call void @escaped_bad_outer(ptr %a, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: Entering function: escaped_bad_outer
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT:   ptr %raw = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: Entering function: identity
+; CHECK-NEXT:   ptr %p = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' in activation #2 based on node #1
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: NoAlias: ended activation #2
+; CHECK-NEXT: Exiting function: identity
+; CHECK-NEXT:   %q = call ptr @identity(ptr %p) => ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #2 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %q, align 4
+; CHECK-NEXT: NoAlias: noalias violation: write through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i32 2, ptr %raw, align 4 at @escaped_bad_outer <stdin>:13
+; CHECK-NEXT: #1   call void @escaped_bad_outer(ptr %a, ptr %a) at @main <stdin>:19
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_wildcard.ll b/llvm/test/tools/llubi/noalias_wildcard.ll
new file mode 100644
index 00000000000000..6a4e83676e4340
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_wildcard.ll
@@ -0,0 +1,36 @@
+; 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
+
+; An inttoptr argument must get a noalias node even before its object is resolved.
+
+define void @write_read(ptr noalias %p, ptr %q) {
+  store i32 1, ptr %p
+  %v = load i32, ptr %q
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  %n = ptrtoint ptr %a to i64
+  %p = inttoptr i64 %n to ptr
+  call void @write_read(ptr %p, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT:   %n = ptrtoint ptr %a to i64 => i64 8
+; CHECK-NEXT:   %p = inttoptr i64 %n to ptr => ptr 0x8 [wildcard]
+; CHECK-NEXT: Entering function: write_read
+; CHECK-NEXT:   ptr %p = ptr 0x8 [wildcard]
+; CHECK-NEXT:   ptr %q = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for wildcard provenance in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through node #1 on 'a' bytes [0, 4): unaccessed -> access including a write by node #1
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %p, align 4
+; CHECK-NEXT: NoAlias: noalias violation: read through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %v = load i32, ptr %q, align 4 at @write_read <stdin>:8
+; CHECK-NEXT: #1   call void @write_read(ptr %p, ptr %a) at @main <stdin>:16
+; CHECK-NEXT: Immediate UB detected: noalias violation: read through raw/root on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_wildcard_foreign_first.ll b/llvm/test/tools/llubi/noalias_wildcard_foreign_first.ll
new file mode 100644
index 00000000000000..0223186292a8eb
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_wildcard_foreign_first.ll
@@ -0,0 +1,36 @@
+; 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
+
+; Foreign accesses must be recorded before the first wildcard access.
+
+define void @write_read(ptr noalias %p, ptr %q) {
+  store i32 1, ptr %q
+  %v = load i32, ptr %p
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  %n = ptrtoint ptr %a to i64
+  %p = inttoptr i64 %n to ptr
+  call void @write_read(ptr %p, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT:   %n = ptrtoint ptr %a to i64 => i64 8
+; CHECK-NEXT:   %p = inttoptr i64 %n to ptr => ptr 0x8 [wildcard]
+; CHECK-NEXT: Entering function: write_read
+; CHECK-NEXT:   ptr %p = ptr 0x8 [wildcard]
+; CHECK-NEXT:   ptr %q = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for wildcard provenance in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through raw/root on 'a' bytes [0, 4): unaccessed -> access including a write by raw/root
+; CHECK-NEXT: NoAlias: write through raw/root on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %q, align 4
+; CHECK-NEXT: NoAlias: noalias violation: read through node #1 on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %v = load i32, ptr %p, align 4 at @write_read <stdin>:8
+; CHECK-NEXT: #1   call void @write_read(ptr %p, ptr %a) at @main <stdin>:16
+; CHECK-NEXT: Immediate UB detected: noalias violation: read through node #1 on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_wildcard_outside_allocation.ll b/llvm/test/tools/llubi/noalias_wildcard_outside_allocation.ll
new file mode 100644
index 00000000000000..6f97af0b5df8f8
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_wildcard_outside_allocation.ll
@@ -0,0 +1,40 @@
+; 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
+
+; Retagging an out-of-bounds wildcard must not require dereferenceability.
+
+define void @write_read(ptr noalias %p, ptr %q) {
+  store i32 1, ptr %q
+  %inside = getelementptr i8, ptr %p, i64 -4096
+  %v = load i32, ptr %inside
+  ret void
+}
+
+define void @main() {
+  %a = alloca i32
+  %n = ptrtoint ptr %a to i64
+  %p = inttoptr i64 %n to ptr
+  %outside = getelementptr i8, ptr %p, i64 4096
+  call void @write_read(ptr %outside, ptr %a)
+  ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT:   %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT:   %n = ptrtoint ptr %a to i64 => i64 8
+; CHECK-NEXT:   %p = inttoptr i64 %n to ptr => ptr 0x8 [wildcard]
+; CHECK-NEXT:   %outside = getelementptr i8, ptr %p, i64 4096 => ptr 0x1008 [wildcard]
+; CHECK-NEXT: Entering function: write_read
+; CHECK-NEXT:   ptr %p = ptr 0x1008 [wildcard]
+; CHECK-NEXT:   ptr %q = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for wildcard provenance in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 write through raw/root on 'a' bytes [0, 4): unaccessed -> access including a write by raw/root
+; CHECK-NEXT: NoAlias: write through raw/root on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %q, align 4
+; CHECK-NEXT:   %inside = getelementptr i8, ptr %p, i64 -4096 => ptr 0x8 [wildcard]
+; CHECK-NEXT: NoAlias: noalias violation: read through node #1 on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %v = load i32, ptr %inside, align 4 at @write_read <stdin>:9
+; CHECK-NEXT: #1   call void @write_read(ptr %outside, ptr %a) at @main <stdin>:18
+; CHECK-NEXT: Immediate UB detected: noalias violation: read through node #1 on 'a' bytes [0, 4) combines multiple access classes with a write in activation #1
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_wildcard_valid.ll b/llvm/test/tools/llubi/noalias_wildcard_valid.ll
new file mode 100644
index 00000000000000..b076fa98382596
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_wildcard_valid.ll
@@ -0,0 +1,69 @@
+; 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
+
+; Wildcard nodes do not make unrelated or read-only accesses conflict. A frame
+; can contain both wildcard and concrete noalias arguments to the same object.
+
+define void @read_both(ptr noalias %p, ptr noalias %q) {
+  %x = load i32, ptr %p
+  %y = load i32, ptr %q
+  ret void
+}
+
+define void @write_disjoint(ptr noalias %p, ptr noalias %q) {
+  store i32 1, ptr %p
+  store i32 2, ptr %q
+  ret void
+}
+
+define void @main() {
+  %a = alloca [2 x i32]
+  store [2 x i32] zeroinitializer, ptr %a
+  %b = getelementptr i32, ptr %a, i64 1
+  %n = ptrtoint ptr %a to i64
+  %p = inttoptr i64 %n to ptr
+  call void @read_both(ptr %p, ptr %a)
+  call void @write_disjoint(ptr %p, ptr %b)
+  %result = 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:   store [2 x i32] zeroinitializer, ptr %a, align 4
+; CHECK-NEXT:   %b = getelementptr i32, ptr %a, i64 1 => ptr 0xC [a + 4]
+; CHECK-NEXT:   %n = ptrtoint ptr %a to i64 => i64 8
+; CHECK-NEXT:   %p = inttoptr i64 %n to ptr => ptr 0x8 [wildcard]
+; CHECK-NEXT: Entering function: read_both
+; CHECK-NEXT:   ptr %p = ptr 0x8 [wildcard]
+; CHECK-NEXT:   ptr %q = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for wildcard provenance in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' in activation #1 based on raw/root
+; CHECK-NEXT: NoAlias: activation #1 read through node #1 on 'a' bytes [0, 4): unaccessed -> reads by node #1
+; CHECK-NEXT: NoAlias: read through node #1 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   %x = load i32, ptr %p, align 4 => i32 0
+; CHECK-NEXT: NoAlias: activation #1 read through node #2 on 'a' bytes [0, 4): reads by node #1 -> reads by multiple access classes
+; CHECK-NEXT: NoAlias: read through node #2 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   %y = load i32, ptr %q, align 4 => i32 0
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended activation #1
+; CHECK-NEXT: Exiting function: read_both
+; CHECK-NEXT:   call void @read_both(ptr %p, ptr %a)
+; CHECK-NEXT: Entering function: write_disjoint
+; CHECK-NEXT:   ptr %p = ptr 0x8 [wildcard]
+; CHECK-NEXT:   ptr %q = ptr 0xC [a + 4]
+; CHECK-NEXT: NoAlias: created protector node #3 for wildcard provenance in activation #2 based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #4 for 'a' in activation #2 based on raw/root
+; CHECK-NEXT: NoAlias: activation #2 write through node #3 on 'a' bytes [0, 4): unaccessed -> access including a write by node #3
+; CHECK-NEXT: NoAlias: write through node #3 on 'a' bytes [0, 4) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 1, ptr %p, align 4
+; CHECK-NEXT: NoAlias: activation #2 write through node #4 on 'a' bytes [4, 8): unaccessed -> access including a write by node #4
+; CHECK-NEXT: NoAlias: write through node #4 on 'a' bytes [4, 8) checked 1 active noalias activation
+; CHECK-NEXT:   store i32 2, ptr %q, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: NoAlias: ended activation #2
+; CHECK-NEXT: Exiting function: write_disjoint
+; CHECK-NEXT:   call void @write_disjoint(ptr %p, ptr %b)
+; CHECK-NEXT:   %result = 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/tools/llubi/lib/Context.cpp b/llvm/tools/llubi/lib/Context.cpp
index 6aec7e413068f3..14d09d91f8c701 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -1379,9 +1379,8 @@ bool MemoryObject::isHeapAllocated() const {
 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.
+  // Inactive nodes retain parent links: returned and escaped pointers can
+  // still be based on an active ancestor.
   for (uint64_t NodeID = Descendant; NodeID;) {
     if (NodeID == Ancestor)
       return true;
@@ -1393,39 +1392,14 @@ bool Context::isNoAliasAncestor(uint64_t Ancestor, uint64_t Descendant) const {
   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";
+  case NoAliasAccessKind::Deallocate:
+    return "deallocation";
   }
   llvm_unreachable("Unknown NoAliasAccessKind");
 }
@@ -1467,7 +1441,7 @@ uint64_t Context::classifyNoAliasAccess(const NoAliasActivation &Activation,
   for (uint64_t NodeID : Activation.Nodes) {
     const auto It = NoAliasNodes.find(NodeID);
     if (It == NoAliasNodes.end() || !It->second.Active ||
-        It->second.Object != &MO)
+        (It->second.Object && It->second.Object != &MO))
       continue;
     if (isNoAliasAncestor(NodeID, AccessNode))
       return NodeID;
@@ -1510,7 +1484,7 @@ bool Context::updateNoAliasAccesses(NoAliasActivation &Activation,
     if (RunBegin == RunEnd)
       return true;
 
-    const bool IsWrite = Kind == NoAliasAccessKind::Write;
+    const bool IsWrite = Kind != NoAliasAccessKind::Read;
     NoAliasAccessSummary New{AccessClass, false, IsWrite};
     if (Old) {
       New = *Old;
@@ -1615,9 +1589,13 @@ Pointer Context::createNoAliasPointer(const Pointer &Ptr,
   if (!ExperimentalNoAlias || !ActivationID)
     return Ptr;
 
-  MemoryObject *MO = Ptr.getMemoryObject();
-  if (!MO)
+  if (!Ptr.getMemoryObject() && !Ptr.provenance().isWildcard())
     return Ptr;
+  // An unresolved wildcard can reach an allocation via arithmetic before its
+  // first access. Track foreign accesses from entry, without resolving or
+  // dereferencing the parameter.
+  MemoryObject *MO =
+      Ptr.provenance().isWildcard() ? nullptr : Ptr.getMemoryObject();
 
   auto ActivationIt = NoAliasActivations.find(ActivationID);
   assert(ActivationIt != NoAliasActivations.end() &&
@@ -1636,7 +1614,8 @@ Pointer Context::createNoAliasPointer(const Pointer &Ptr,
   NoAliasNodes.try_emplace(NodeID, std::move(Node));
   ActivationIt->second.Nodes.push_back(NodeID);
 
-  auto &Activations = NoAliasActivationsByObject[MO];
+  auto &Activations =
+      MO ? NoAliasActivationsByObject[MO] : WildcardNoAliasActivations;
   if (std::find(Activations.begin(), Activations.end(), ActivationID) ==
       Activations.end())
     Activations.push_back(ActivationID);
@@ -1644,7 +1623,7 @@ Pointer Context::createNoAliasPointer(const Pointer &Ptr,
   std::string S;
   raw_string_ostream OS(S);
   OS << "created protector " << getNoAliasNodeName(NodeID) << " for "
-     << getNoAliasObjectName(*MO) << " in "
+     << (MO ? getNoAliasObjectName(*MO) : "wildcard provenance") << " in "
      << getNoAliasActivationName(ActivationID) << " based on "
      << getNoAliasNodeName(Parent);
   appendNoAliasEvent(std::move(S));
@@ -1657,12 +1636,20 @@ bool Context::accessNoAlias(MemoryObject &MO, uint64_t Offset, uint64_t Size,
     return true;
 
   auto It = NoAliasActivationsByObject.find(&MO);
-  if (It == NoAliasActivationsByObject.end())
+  if (It == NoAliasActivationsByObject.end() &&
+      WildcardNoAliasActivations.empty())
     return true;
 
+  SmallVector<uint64_t, 4> Activations(WildcardNoAliasActivations.begin(),
+                                       WildcardNoAliasActivations.end());
+  if (It != NoAliasActivationsByObject.end())
+    for (uint64_t ID : It->second)
+      if (!is_contained(Activations, ID))
+        Activations.push_back(ID);
+
   const uint64_t End = Offset + Size;
   uint32_t CheckedActivations = 0;
-  for (uint64_t ActivationID : It->second) {
+  for (uint64_t ActivationID : Activations) {
     auto ActivationIt = NoAliasActivations.find(ActivationID);
     if (ActivationIt == NoAliasActivations.end())
       continue;
@@ -1703,7 +1690,6 @@ void Context::endNoAliasActivation(uint64_t ActivationID) {
       continue;
     MemoryObject *MO = NodeIt->second.Object;
     NodeIt->second.Active = false;
-    tryEraseInactiveNoAliasNode(NodeID);
     auto ObjectIt = NoAliasActivationsByObject.find(MO);
     if (ObjectIt == NoAliasActivationsByObject.end())
       continue;
@@ -1712,8 +1698,14 @@ void Context::endNoAliasActivation(uint64_t ActivationID) {
     if (IDs.empty())
       NoAliasActivationsByObject.erase(ObjectIt);
   }
+  llvm::erase(WildcardNoAliasActivations, ActivationID);
   appendNoAliasEvent("ended " + getNoAliasActivationName(ActivationID));
   NoAliasActivations.erase(ActivationIt);
+  // A node ID may survive in SSA values or memory after its call returns.
+  // Reclaim the ancestry only when no active protector can distinguish it
+  // from the root. IDs are never reused, so later retags can safely use root.
+  if (NoAliasActivations.empty())
+    NoAliasNodes.clear();
 }
 
 SmallVector<std::string, 4> Context::takeNoAliasEvents() {
@@ -1726,23 +1718,13 @@ void Context::clearNoAliasState(const MemoryObject &MO) {
   if (!ExperimentalNoAlias)
     return;
 
-  const auto It = NoAliasActivationsByObject.find(&MO);
-  if (It == NoAliasActivationsByObject.end())
-    return;
-  SmallVector<uint64_t, 4> ActivationIDs(It->second.begin(), It->second.end());
-  for (uint64_t ActivationID : ActivationIDs) {
-    auto ActivationIt = NoAliasActivations.find(ActivationID);
-    if (ActivationIt == NoAliasActivations.end())
-      continue;
-    for (uint64_t NodeID : ActivationIt->second.Nodes) {
-      auto NodeIt = NoAliasNodes.find(NodeID);
-      if (NodeIt != NoAliasNodes.end() && NodeIt->second.Object == &MO)
-        NodeIt->second.Active = false;
-      tryEraseInactiveNoAliasNode(NodeID);
-    }
-    ActivationIt->second.Accesses.erase(const_cast<MemoryObject *>(&MO));
-  }
-  NoAliasActivationsByObject.erase(It);
+  for (auto &[ID, Activation] : NoAliasActivations)
+    Activation.Accesses.erase(const_cast<MemoryObject *>(&MO));
+  // No valid access can use a concrete pointer to this allocation again.
+  // Also remove inactive nodes before their raw Object pointer can dangle.
+  NoAliasNodes.remove_if(
+      [&](const auto &Entry) { return Entry.second.Object == &MO; });
+  NoAliasActivationsByObject.erase(const_cast<MemoryObject *>(&MO));
 }
 
 } // namespace llvm::ubi
diff --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index d4195402f75f36..028ceb77c81604 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -102,7 +102,7 @@ struct ProgramExitInfo {
   }
 };
 
-enum class NoAliasAccessKind { Read, Write };
+enum class NoAliasAccessKind { Read, Write, Deallocate };
 
 class MemoryObject : public RefCountedBase<MemoryObject> {
   uint64_t Address;
@@ -323,6 +323,8 @@ class Context {
   /// they are used only to classify accesses in active function activations.
   struct NoAliasNode {
     uint64_t Parent = 0;
+    // Null for wildcard provenance: this node may access any allocation its
+    // provenance permits, including one reached by pointer arithmetic.
     MemoryObject *Object = nullptr;
     bool Active = false;
   };
@@ -338,6 +340,7 @@ class Context {
   DenseMap<uint64_t, NoAliasNode> NoAliasNodes;
   DenseMap<uint64_t, NoAliasActivation> NoAliasActivations;
   DenseMap<MemoryObject *, SmallVector<uint64_t, 2>> NoAliasActivationsByObject;
+  SmallVector<uint64_t, 2> WildcardNoAliasActivations;
 
   // noalias-related diagnostics
   std::string LastNoAliasError;
@@ -359,9 +362,6 @@ class Context {
   /// 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 std::string getNoAliasNodeName(uint64_t NodeID);
   static std::string getNoAliasActivationName(uint64_t ActivationID);
@@ -517,7 +517,8 @@ class Context {
   /// Apply an access to every active activation protecting \p MO.
   bool accessNoAlias(MemoryObject &MO, uint64_t Offset, uint64_t Size,
                      uint64_t AccessNode, NoAliasAccessKind Kind);
-  /// End an activation and discard its access summaries.
+  /// End an activation and discard its access summaries, preserving ancestry
+  /// for escaped pointers while any other activation remains active.
   void endNoAliasActivation(uint64_t ActivationID);
   StringRef getLastNoAliasError() const { return LastNoAliasError; }
   SmallVector<std::string, 4> takeNoAliasEvents();
diff --git a/llvm/tools/llubi/lib/ExecutorBase.cpp b/llvm/tools/llubi/lib/ExecutorBase.cpp
index fc634830aa0892..c0b496a488a2af 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.cpp
+++ b/llvm/tools/llubi/lib/ExecutorBase.cpp
@@ -13,30 +13,38 @@
 #include "ExecutorBase.h"
 
 namespace llvm::ubi {
+
+uint64_t retagNoAliasArguments(Context &Ctx, Function &F, CallBase *CallSite,
+                               MutableArrayRef<AnyValue> Args) {
+  if (!Ctx.isExperimentalNoAliasEnabled())
+    return 0;
+  uint64_t Activation = 0;
+  for (auto [I, ArgValue] : enumerate(Args)) {
+    bool NoAlias = F.getAttributes().hasParamAttr(I, Attribute::NoAlias) ||
+                   (CallSite && CallSite->getAttributes().hasParamAttr(
+                                    I, Attribute::NoAlias));
+    if (!NoAlias || !ArgValue.isPointer())
+      continue;
+    if (!Activation)
+      Activation = Ctx.beginNoAliasActivation();
+    ArgValue = Ctx.createNoAliasPointer(ArgValue.asPointer(), Activation);
+  }
+  return Activation;
+}
+
 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(Ctx.getTLIImpl(), &F) {
+    : Func(F), LastFrame(LastFrame), CallSite(CallSite),
+      Args(Args.begin(), Args.end()), 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()) {
-    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() &&
-        ArgValue.asPointer().getMemoryObject()) {
-      if (!NoAliasActivation)
-        NoAliasActivation = Ctx.beginNoAliasActivation();
-      ArgValue =
-          Ctx.createNoAliasPointer(ArgValue.asPointer(), NoAliasActivation);
-    }
-    ValueMap[&Arg] = std::move(ArgValue);
-  }
+  NoAliasActivation = retagNoAliasArguments(Ctx, F, CallSite, this->Args);
+  for (Argument &Arg : F.args())
+    ValueMap[&Arg] = this->Args[Arg.getArgNo()];
 }
 
 DiagnosticReporter ExecutorBase::reportImmediateUB() {
@@ -129,9 +137,24 @@ ExecutorBase::verifyMemAccess(const Pointer &Ptr, uint64_t AccessSize,
     return {};
   }
 
+  if (!verifyNoAliasAccess(*MO, Offset.getZExtValue(), AccessSize, Ptr,
+                           IsStore ? NoAliasAccessKind::Write
+                                   : NoAliasAccessKind::Read))
+    return {};
   return {MO, Offset.getZExtValue()};
 }
 
+bool ExecutorBase::verifyNoAliasAccess(MemoryObject &MO, uint64_t Offset,
+                                       uint64_t Size, const Pointer &Ptr,
+                                       NoAliasAccessKind Kind) {
+  bool Valid =
+      Ctx.accessNoAlias(MO, Offset, Size, Ptr.getNoAliasNodeID(), Kind);
+  flushNoAliasEvents();
+  if (!Valid)
+    reportImmediateUB() << Ctx.getLastNoAliasError();
+  return Valid;
+}
+
 AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
                             bool NoUndef) {
   if (Ptr.isPoison()) {
@@ -149,17 +172,6 @@ AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
     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);
@@ -176,14 +188,6 @@ void ExecutorBase::store(const AnyValue &Ptr, Align Alignment,
           PtrVal, Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
           /*IsStore=*/true);
       MO) {
-    if (const uint64_t AccessSize = Ctx.getEffectiveTypeStoreSize(ValTy);
-        !Ctx.accessNoAlias(*MO, Offset, AccessSize, PtrVal.getNoAliasNodeID(),
-                           NoAliasAccessKind::Write)) {
-      flushNoAliasEvents();
-      reportImmediateUB() << Ctx.getLastNoAliasError();
-      return;
-    }
-    flushNoAliasEvents();
     Ctx.store(*MO, Offset, Val, ValTy);
   }
 }
diff --git a/llvm/tools/llubi/lib/ExecutorBase.h b/llvm/tools/llubi/lib/ExecutorBase.h
index 370808d77814b0..b514f7a17dd0df 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.h
+++ b/llvm/tools/llubi/lib/ExecutorBase.h
@@ -22,6 +22,11 @@
 
 namespace llvm::ubi {
 
+/// Apply the parameter guarantees of this invocation to its callee-visible
+/// arguments, creating at most one dynamic noalias activation.
+uint64_t retagNoAliasArguments(Context &Ctx, Function &F, CallBase *CallSite,
+                               MutableArrayRef<AnyValue> Args);
+
 enum class FrameState {
   // It is about to enter the function.
   // Valid transition:
@@ -48,7 +53,7 @@ struct Frame {
   Function &Func;
   Frame *LastFrame;
   CallBase *CallSite;
-  ArrayRef<AnyValue> Args;
+  SmallVector<AnyValue, 4> Args;
   AnyValue &RetVal;
 
   TargetLibraryInfo TLI;
@@ -105,13 +110,18 @@ class ExecutorBase {
 
   void flushNoAliasEvents();
 
-  /// Check if the upcoming memory access is valid. Returns the resolved memory
-  /// object and offset if it is valid.
+  /// Validate a memory access and record its noalias effect. Returns the
+  /// resolved memory object and offset if it is valid. Call this only for
+  /// actual accesses, not speculative bounds or provenance queries.
   std::pair<MemoryObject *, uint64_t> verifyMemAccess(const Pointer &Ptr,
                                                       uint64_t AccessSize,
                                                       Align Alignment,
                                                       bool IsStore);
 
+  /// Record an otherwise valid access, including a deallocation.
+  bool verifyNoAliasAccess(MemoryObject &MO, uint64_t Offset, uint64_t Size,
+                           const Pointer &Ptr, NoAliasAccessKind Kind);
+
   AnyValue load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
                 bool NoUndef);
   void store(const AnyValue &Ptr, Align Alignment, const AnyValue &Val,
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index e15f4f0b197e4a..29793a3af425b1 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -2164,12 +2164,23 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     }
 
     CurrentFrame->ResolvedCallee = Callee;
+    // Intrinsics and library calls have no Frame, but their parameter
+    // guarantees (including call-site-only attributes) cover their effects.
+    uint64_t NoAliasActivation = 0;
+    if (Callee->isDeclaration()) {
+      NoAliasActivation = retagNoAliasArguments(Ctx, *Callee, &CB, CalleeArgs);
+      flushNoAliasEvents();
+    }
     if (Callee->isIntrinsic()) {
       CurrentFrame->CalleeRetVal = callIntrinsic(CB, CalleeArgs);
+      Ctx.endNoAliasActivation(NoAliasActivation);
+      flushNoAliasEvents();
       returnFromCallee();
       return;
     } else if (Callee->isDeclaration()) {
       CurrentFrame->CalleeRetVal = callLibFunc(CB, Callee, CalleeArgs);
+      Ctx.endNoAliasActivation(NoAliasActivation);
+      flushNoAliasEvents();
       returnFromCallee();
       return;
     } else {
diff --git a/llvm/tools/llubi/lib/Library.cpp b/llvm/tools/llubi/lib/Library.cpp
index c3452b8af92c01..82e9caa17ee1a6 100644
--- a/llvm/tools/llubi/lib/Library.cpp
+++ b/llvm/tools/llubi/lib/Library.cpp
@@ -158,6 +158,10 @@ AnyValue Library::executeFree(ArrayRef<AnyValue> Args) {
   // allocation family (malloc, calloc, etc.) is freed with a different free
   // function comes from a different family (C++ delete, etc.)
 
+  if (!Executor.verifyNoAliasAccess(*Obj, 0, Obj->getSize(), Ptr,
+                                    NoAliasAccessKind::Deallocate))
+    return AnyValue();
+
   if (!Ctx.free(*Obj)) {
     Executor.reportImmediateUB()
         << "freeing an invalid pointer at 0x"

>From 724d50de72f828836dc55659c2474b1b29f68710 Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhigec_cpp at outlook.com>
Date: Sat, 26 Sep 2026 17:57:35 +0800
Subject: [PATCH 5/5] [llubi] Format code

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

diff --git a/llvm/tools/llubi/lib/Value.h b/llvm/tools/llubi/lib/Value.h
index 739574e698d0b6..c5b6d5d6573a5d 100644
--- a/llvm/tools/llubi/lib/Value.h
+++ b/llvm/tools/llubi/lib/Value.h
@@ -146,7 +146,8 @@ struct Byte {
                 static_cast<uint8_t>(Value << Shift),
                 static_cast<uint8_t>(TagMask << Shift),
                 static_cast<uint8_t>(TagValue << Shift),
-                static_cast<uint8_t>(NoAliasMask << Shift), NoAliasNode};
+                static_cast<uint8_t>(NoAliasMask << Shift),
+                NoAliasNode};
   }
 
   bool areHighBitsZExtd(uint8_t BitsFrom) const {



More information about the llvm-commits mailing list