[llvm] [llubi] Experimental support for noalias (PR #195808)
Zhige Chen via llvm-commits
llvm-commits at lists.llvm.org
Sun Jul 19 22:46:08 PDT 2026
https://github.com/nofe1248 updated https://github.com/llvm/llvm-project/pull/195808
>From 6fe3a0924ef21937eb9bfc08651620dd24b7b97d 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/2] [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 | 61 ++-
llvm/tools/llubi/llubi.cpp | 10 +
16 files changed, 1081 insertions(+), 29 deletions(-)
create mode 100644 llvm/test/tools/llubi/noalias_after_return.ll
create mode 100644 llvm/test/tools/llubi/noalias_aliasing_reads.ll
create mode 100644 llvm/test/tools/llubi/noalias_disabled_by_default.ll
create mode 100644 llvm/test/tools/llubi/noalias_disjoint_intervals.ll
create mode 100644 llvm/test/tools/llubi/noalias_foreign_access.ll
create mode 100644 llvm/test/tools/llubi/noalias_outside_range.ll
create mode 100644 llvm/test/tools/llubi/noalias_overlapping_ranges.ll
create mode 100644 llvm/test/tools/llubi/noalias_prune_stale_node.ll
create mode 100644 llvm/test/tools/llubi/noalias_reserved_foreign_read.ll
diff --git a/llvm/test/tools/llubi/noalias_after_return.ll b/llvm/test/tools/llubi/noalias_after_return.ll
new file mode 100644
index 0000000000000..6738a951b53c6
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_after_return.ll
@@ -0,0 +1,35 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --experimental-noalias --verbose < %s 2>&1 | FileCheck %s
+
+define void @write_one(ptr noalias %x) {
+ store i32 1, ptr %x
+ ret void
+}
+
+define void @main() {
+ %a = alloca i32
+ call void @write_one(ptr %a)
+ %v = load i32, ptr %a
+ store i32 2, ptr %a
+ ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT: %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: Entering function: write_one
+; CHECK-NEXT: ptr %x = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' based on raw/root
+; CHECK-NEXT: NoAlias: node #1 local write through node #1 on 'a' bytes [0, 4): Reserved -> Unique
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias protector
+; CHECK-NEXT: store i32 1, ptr %x, align 4
+; CHECK-NEXT: ret void
+; CHECK-NEXT: NoAlias: protector end for node #1 triggers synthetic write on 'a' bytes [0, 4)
+; CHECK-NEXT: NoAlias: protector end: node #1 local write through node #1 on 'a' bytes [0, 4): Unique -> Unique
+; CHECK-NEXT: NoAlias: ended protector node #1
+; CHECK-NEXT: NoAlias: erased inactive protector node #1
+; CHECK-NEXT: Exiting function: write_one
+; CHECK-NEXT: call void @write_one(ptr %a)
+; CHECK-NEXT: %v = load i32, ptr %a, align 4 => i32 1
+; CHECK-NEXT: store i32 2, ptr %a, align 4
+; CHECK-NEXT: ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_aliasing_reads.ll b/llvm/test/tools/llubi/noalias_aliasing_reads.ll
new file mode 100644
index 0000000000000..c1fcd813c0211
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_aliasing_reads.ll
@@ -0,0 +1,46 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --experimental-noalias --verbose < %s 2>&1 | FileCheck %s
+
+define void @read_both(ptr noalias %x, ptr noalias %y) {
+ %vx = load i32, ptr %x
+ %vy = load i32, ptr %y
+ ret void
+}
+
+define void @main() {
+ %a = alloca i32
+ store i32 42, ptr %a
+ call void @read_both(ptr %a, ptr %a)
+ ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT: %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: store i32 42, ptr %a, align 4
+; CHECK-NEXT: Entering function: read_both
+; CHECK-NEXT: ptr %x = ptr 0x8 [a]
+; CHECK-NEXT: ptr %y = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' based on raw/root
+; CHECK-NEXT: NoAlias: node #1 local read through node #1 on 'a' bytes [0, 4): Reserved -> ReservedL
+; CHECK-NEXT: NoAlias: node #2 foreign read through node #1 on 'a' bytes [0, 4): Reserved -> ReservedF
+; CHECK-NEXT: NoAlias: read through node #1 on 'a' bytes [0, 4) checked 2 active noalias protectors
+; CHECK-NEXT: %vx = load i32, ptr %x, align 4 => i32 42
+; CHECK-NEXT: NoAlias: node #1 foreign read through node #2 on 'a' bytes [0, 4): ReservedL -> ReservedLF
+; CHECK-NEXT: NoAlias: node #2 local read through node #2 on 'a' bytes [0, 4): ReservedF -> ReservedLF
+; CHECK-NEXT: NoAlias: read through node #2 on 'a' bytes [0, 4) checked 2 active noalias protectors
+; CHECK-NEXT: %vy = load i32, ptr %y, align 4 => i32 42
+; CHECK-NEXT: ret void
+; CHECK-NEXT: NoAlias: protector end for node #1 triggers synthetic read on 'a' bytes [0, 4)
+; CHECK-NEXT: NoAlias: protector end: node #1 local read through node #1 on 'a' bytes [0, 4): ReservedLF -> ReservedLF
+; CHECK-NEXT: NoAlias: protector end: node #2 foreign read through node #1 on 'a' bytes [0, 4): ReservedLF -> ReservedLF
+; CHECK-NEXT: NoAlias: ended protector node #1
+; CHECK-NEXT: NoAlias: erased inactive protector node #1
+; CHECK-NEXT: NoAlias: protector end for node #2 triggers synthetic read on 'a' bytes [0, 4)
+; CHECK-NEXT: NoAlias: protector end: node #2 local read through node #2 on 'a' bytes [0, 4): ReservedLF -> ReservedLF
+; CHECK-NEXT: NoAlias: ended protector node #2
+; CHECK-NEXT: NoAlias: erased inactive protector node #2
+; CHECK-NEXT: Exiting function: read_both
+; CHECK-NEXT: call void @read_both(ptr %a, ptr %a)
+; CHECK-NEXT: ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_disabled_by_default.ll b/llvm/test/tools/llubi/noalias_disabled_by_default.ll
new file mode 100644
index 0000000000000..65ce89ac44636
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_disabled_by_default.ll
@@ -0,0 +1,29 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --verbose < %s 2>&1 | FileCheck %s
+
+define void @violates_if_enabled(ptr noalias %x, ptr %y) {
+ store i32 1, ptr %x
+ %v = load i32, ptr %y
+ ret void
+}
+
+define void @main() {
+ %a = alloca i32
+ store i32 0, ptr %a
+ call void @violates_if_enabled(ptr %a, ptr %a)
+ ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT: %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: store i32 0, ptr %a, align 4
+; CHECK-NEXT: Entering function: violates_if_enabled
+; CHECK-NEXT: ptr %x = ptr 0x8 [a]
+; CHECK-NEXT: ptr %y = ptr 0x8 [a]
+; CHECK-NEXT: store i32 1, ptr %x, align 4
+; CHECK-NEXT: %v = load i32, ptr %y, align 4 => i32 1
+; CHECK-NEXT: ret void
+; CHECK-NEXT: Exiting function: violates_if_enabled
+; CHECK-NEXT: call void @violates_if_enabled(ptr %a, ptr %a)
+; CHECK-NEXT: ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_disjoint_intervals.ll b/llvm/test/tools/llubi/noalias_disjoint_intervals.ll
new file mode 100644
index 0000000000000..dd9030e9ea10f
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_disjoint_intervals.ll
@@ -0,0 +1,49 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --experimental-noalias --verbose < %s 2>&1 | FileCheck %s
+
+define void @write_disjoint(ptr noalias %x, ptr noalias %y) {
+ store i32 1, ptr %x
+ store i32 2, ptr %y
+ ret void
+}
+
+define void @main() {
+ %a = alloca [2 x i32]
+ %x = getelementptr [2 x i32], ptr %a, i64 0, i64 0
+ %y = getelementptr [2 x i32], ptr %a, i64 0, i64 1
+ call void @write_disjoint(ptr %x, ptr %y)
+ %v = load [2 x i32], ptr %a
+ ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT: %a = alloca [2 x i32], align 4 => ptr 0x8 [a]
+; CHECK-NEXT: %x = getelementptr [2 x i32], ptr %a, i64 0, i64 0 => ptr 0x8 [a]
+; CHECK-NEXT: %y = getelementptr [2 x i32], ptr %a, i64 0, i64 1 => ptr 0xC [a + 4]
+; CHECK-NEXT: Entering function: write_disjoint
+; CHECK-NEXT: ptr %x = ptr 0x8 [a]
+; CHECK-NEXT: ptr %y = ptr 0xC [a + 4]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' based on raw/root
+; CHECK-NEXT: NoAlias: node #1 local write through node #1 on 'a' bytes [0, 4): Reserved -> Unique
+; CHECK-NEXT: NoAlias: node #2 foreign write through node #1 on 'a' bytes [0, 4): Reserved -> Disabled
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 2 active noalias protectors
+; CHECK-NEXT: store i32 1, ptr %x, align 4
+; CHECK-NEXT: NoAlias: node #1 foreign write through node #2 on 'a' bytes [4, 8): Reserved -> Disabled
+; CHECK-NEXT: NoAlias: node #2 local write through node #2 on 'a' bytes [4, 8): Reserved -> Unique
+; CHECK-NEXT: NoAlias: write through node #2 on 'a' bytes [4, 8) checked 2 active noalias protectors
+; CHECK-NEXT: store i32 2, ptr %y, align 4
+; CHECK-NEXT: ret void
+; CHECK-NEXT: NoAlias: protector end for node #1 triggers synthetic write on 'a' bytes [0, 4)
+; CHECK-NEXT: NoAlias: protector end: node #1 local write through node #1 on 'a' bytes [0, 4): Unique -> Unique
+; CHECK-NEXT: NoAlias: protector end: node #2 foreign write through node #1 on 'a' bytes [0, 4): Disabled -> Disabled
+; CHECK-NEXT: NoAlias: ended protector node #1
+; CHECK-NEXT: NoAlias: erased inactive protector node #1
+; CHECK-NEXT: NoAlias: protector end for node #2 triggers synthetic write on 'a' bytes [4, 8)
+; CHECK-NEXT: NoAlias: protector end: node #2 local write through node #2 on 'a' bytes [4, 8): Unique -> Unique
+; CHECK-NEXT: NoAlias: ended protector node #2
+; CHECK-NEXT: NoAlias: erased inactive protector node #2
+; CHECK-NEXT: Exiting function: write_disjoint
+; CHECK-NEXT: call void @write_disjoint(ptr %x, ptr %y)
+; CHECK-NEXT: %v = load [2 x i32], ptr %a, align 4 => { i32 1, i32 2 }
+; CHECK-NEXT: ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_foreign_access.ll b/llvm/test/tools/llubi/noalias_foreign_access.ll
new file mode 100644
index 0000000000000..b76effb09d746
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_foreign_access.ll
@@ -0,0 +1,32 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --experimental-noalias --verbose < %s 2>&1 | FileCheck %s
+
+define void @violates(ptr noalias %x, ptr %y) {
+ store i32 1, ptr %x
+ %v = load i32, ptr %y
+ ret void
+}
+
+define void @main() {
+ %a = alloca i32
+ store i32 0, ptr %a
+ call void @violates(ptr %a, ptr %a)
+ ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT: %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: store i32 0, ptr %a, align 4
+; CHECK-NEXT: Entering function: violates
+; CHECK-NEXT: ptr %x = ptr 0x8 [a]
+; CHECK-NEXT: ptr %y = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' based on raw/root
+; CHECK-NEXT: NoAlias: node #1 local write through node #1 on 'a' bytes [0, 4): Reserved -> Unique
+; CHECK-NEXT: NoAlias: write through node #1 on 'a' bytes [0, 4) checked 1 active noalias protector
+; CHECK-NEXT: store i32 1, ptr %x, align 4
+; CHECK-NEXT: NoAlias: noalias violation: read through raw/root on 'a' bytes [0, 4) is foreign to protected node #1, but that protector is in Unique state
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0 %v = load i32, ptr %y, align 4 at @violates
+; CHECK-NEXT: #1 call void @violates(ptr %a, ptr %a) at @main
+; CHECK-NEXT: Immediate UB detected: noalias violation: read through raw/root on 'a' bytes [0, 4) is foreign to protected node #1, but that protector is in Unique state
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_outside_range.ll b/llvm/test/tools/llubi/noalias_outside_range.ll
new file mode 100644
index 0000000000000..aa946d0f11e84
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_outside_range.ll
@@ -0,0 +1,56 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; Ported from Miri's tests/fail/tree_borrows/outside-range.rs:
+; protector checks are byte-range precise. Foreign writes outside the bytes
+; previously used by the protected pointer do not fail, but invalidating an
+; accessed byte does.
+; RUN: not llubi --experimental-noalias --verbose < %s 2>&1 | FileCheck %s
+
+define void @stuff(ptr noalias %x, ptr %raw) {
+ %raw1 = getelementptr i8, ptr %raw, i64 1
+ store i8 42, ptr %raw1
+ %x2 = getelementptr i8, ptr %x, i64 2
+ %v2 = load i8, ptr %x2
+ %x3 = getelementptr i8, ptr %x, i64 3
+ %v3 = load i8, ptr %x3
+ %raw3 = getelementptr i8, ptr %raw, i64 3
+ store i8 42, ptr %raw3
+ ret void
+}
+
+define void @main() {
+ %data = alloca [4 x i8]
+ %raw = getelementptr [4 x i8], ptr %data, i64 0, i64 0
+ call void @stuff(ptr %raw, ptr %raw)
+ ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT: %data = alloca [4 x i8], align 1 => ptr 0x8 [data]
+; CHECK-NEXT: %raw = getelementptr [4 x i8], ptr %data, i64 0, i64 0 => ptr 0x8 [data]
+; CHECK-NEXT: Entering function: stuff
+; CHECK-NEXT: ptr %x = ptr 0x8 [data]
+; CHECK-NEXT: ptr %raw = ptr 0x8 [data]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'data' based on raw/root
+; CHECK-NEXT: %raw1 = getelementptr i8, ptr %raw, i64 1 => ptr 0x9 [data + 1]
+; CHECK-NEXT: NoAlias: node #1 foreign write through raw/root on 'data' bytes [1, 2): Reserved -> Disabled
+; CHECK-NEXT: NoAlias: write through raw/root on 'data' bytes [1, 2) checked 1 active noalias protector
+; CHECK-NEXT: store i8 42, ptr %raw1, align 1
+; CHECK-NEXT: %x2 = getelementptr i8, ptr %x, i64 2 => ptr 0xA [data + 2]
+; CHECK-NEXT: NoAlias: node #1 foreign read through raw/root on 'data' bytes [2, 3): Reserved -> ReservedF
+; CHECK-NEXT: NoAlias: read through raw/root on 'data' bytes [2, 3) checked 1 active noalias protector
+; CHECK-NEXT: %v2 = load i8, ptr %x2, align 1 => i8 62
+; CHECK-NEXT: %x3 = getelementptr i8, ptr %x, i64 3 => ptr 0xB [data + 3]
+; CHECK-NEXT: NoAlias: node #1 foreign read through raw/root on 'data' bytes [3, 4): Reserved -> ReservedF
+; CHECK-NEXT: NoAlias: read through raw/root on 'data' bytes [3, 4) checked 1 active noalias protector
+; CHECK-NEXT: %v3 = load i8, ptr %x3, align 1 => i8 -117
+; CHECK-NEXT: %raw3 = getelementptr i8, ptr %raw, i64 3 => ptr 0xB [data + 3]
+; CHECK-NEXT: NoAlias: node #1 foreign write through raw/root on 'data' bytes [3, 4): ReservedF -> Disabled
+; CHECK-NEXT: NoAlias: write through raw/root on 'data' bytes [3, 4) checked 1 active noalias protector
+; CHECK-NEXT: store i8 42, ptr %raw3, align 1
+; CHECK-NEXT: ret void
+; CHECK-NEXT: NoAlias: ended protector node #1
+; CHECK-NEXT: NoAlias: erased inactive protector node #1
+; CHECK-NEXT: Exiting function: stuff
+; CHECK-NEXT: call void @stuff(ptr %raw, ptr %raw)
+; CHECK-NEXT: ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_overlapping_ranges.ll b/llvm/test/tools/llubi/noalias_overlapping_ranges.ll
new file mode 100644
index 0000000000000..068e4a9136ee9
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_overlapping_ranges.ll
@@ -0,0 +1,46 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; Ported from Miri's tests/fail/both_borrows/buggy_split_at_mut.rs:
+; two supposedly independent mutable ranges overlap, so writes through both
+; protected noalias arguments are rejected.
+; RUN: not llubi --experimental-noalias --verbose < %s 2>&1 | FileCheck %s
+
+define void @write_overlap(ptr noalias %a, ptr noalias %b) {
+ %a1 = getelementptr [4 x i32], ptr %a, i64 0, i64 1
+ store i32 5, ptr %a1
+ %b1 = getelementptr [4 x i32], ptr %b, i64 0, i64 1
+ store i32 6, ptr %b1
+ ret void
+}
+
+define void @main() {
+ %array = alloca [4 x i32]
+ call void @write_overlap(ptr %array, ptr %array)
+ ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT: %array = alloca [4 x i32], align 4 => ptr 0x8 [array]
+; CHECK-NEXT: Entering function: write_overlap
+; CHECK-NEXT: ptr %a = ptr 0x8 [array]
+; CHECK-NEXT: ptr %b = ptr 0x8 [array]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'array' based on raw/root
+; CHECK-NEXT: NoAlias: created protector node #2 for 'array' based on raw/root
+; CHECK-NEXT: %a1 = getelementptr [4 x i32], ptr %a, i64 0, i64 1 => ptr 0xC [array + 4]
+; CHECK-NEXT: NoAlias: node #1 foreign write through raw/root on 'array' bytes [4, 8): Reserved -> Disabled
+; CHECK-NEXT: NoAlias: node #2 foreign write through raw/root on 'array' bytes [4, 8): Reserved -> Disabled
+; CHECK-NEXT: NoAlias: write through raw/root on 'array' bytes [4, 8) checked 2 active noalias protectors
+; CHECK-NEXT: store i32 5, ptr %a1, align 4
+; CHECK-NEXT: %b1 = getelementptr [4 x i32], ptr %b, i64 0, i64 1 => ptr 0xC [array + 4]
+; CHECK-NEXT: NoAlias: node #1 foreign write through raw/root on 'array' bytes [4, 8): Disabled -> Disabled
+; CHECK-NEXT: NoAlias: node #2 foreign write through raw/root on 'array' bytes [4, 8): Disabled -> Disabled
+; CHECK-NEXT: NoAlias: write through raw/root on 'array' bytes [4, 8) checked 2 active noalias protectors
+; CHECK-NEXT: store i32 6, ptr %b1, align 4
+; CHECK-NEXT: ret void
+; CHECK-NEXT: NoAlias: ended protector node #1
+; CHECK-NEXT: NoAlias: erased inactive protector node #1
+; CHECK-NEXT: NoAlias: ended protector node #2
+; CHECK-NEXT: NoAlias: erased inactive protector node #2
+; CHECK-NEXT: Exiting function: write_overlap
+; CHECK-NEXT: call void @write_overlap(ptr %array, ptr %array)
+; CHECK-NEXT: ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_prune_stale_node.ll b/llvm/test/tools/llubi/noalias_prune_stale_node.ll
new file mode 100644
index 0000000000000..da83ef7d01659
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_prune_stale_node.ll
@@ -0,0 +1,51 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --experimental-noalias --verbose < %s 2>&1 | FileCheck %s
+
+define void @capture(ptr noalias %x, ptr %slot) {
+ store ptr %x, ptr %slot
+ ret void
+}
+
+define void @write_one(ptr noalias %x) {
+ store i32 1, ptr %x
+ ret void
+}
+
+define void @main() {
+ %a = alloca i32
+ %slot = alloca ptr
+ call void @capture(ptr %a, ptr %slot)
+ %stale = load ptr, ptr %slot
+ call void @write_one(ptr %stale)
+ ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT: %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: %slot = alloca ptr, align 8 => ptr 0x10 [slot]
+; CHECK-NEXT: Entering function: capture
+; CHECK-NEXT: ptr %x = ptr 0x8 [a]
+; CHECK-NEXT: ptr %slot = ptr 0x10 [slot]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' based on raw/root
+; CHECK-NEXT: store ptr %x, ptr %slot, align 8
+; CHECK-NEXT: ret void
+; CHECK-NEXT: NoAlias: ended protector node #1
+; CHECK-NEXT: NoAlias: erased inactive protector node #1
+; CHECK-NEXT: Exiting function: capture
+; CHECK-NEXT: call void @capture(ptr %a, ptr %slot)
+; CHECK-NEXT: %stale = load ptr, ptr %slot, align 8 => ptr 0x8 [a]
+; CHECK-NEXT: Entering function: write_one
+; CHECK-NEXT: ptr %x = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #2 for 'a' based on raw/root
+; CHECK-NEXT: NoAlias: node #2 local write through node #2 on 'a' bytes [0, 4): Reserved -> Unique
+; CHECK-NEXT: NoAlias: write through node #2 on 'a' bytes [0, 4) checked 1 active noalias protector
+; CHECK-NEXT: store i32 1, ptr %x, align 4
+; CHECK-NEXT: ret void
+; CHECK-NEXT: NoAlias: protector end for node #2 triggers synthetic write on 'a' bytes [0, 4)
+; CHECK-NEXT: NoAlias: protector end: node #2 local write through node #2 on 'a' bytes [0, 4): Unique -> Unique
+; CHECK-NEXT: NoAlias: ended protector node #2
+; CHECK-NEXT: NoAlias: erased inactive protector node #2
+; CHECK-NEXT: Exiting function: write_one
+; CHECK-NEXT: call void @write_one(ptr %stale)
+; CHECK-NEXT: ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/noalias_reserved_foreign_read.ll b/llvm/test/tools/llubi/noalias_reserved_foreign_read.ll
new file mode 100644
index 0000000000000..671659b1759f7
--- /dev/null
+++ b/llvm/test/tools/llubi/noalias_reserved_foreign_read.ll
@@ -0,0 +1,35 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; Adapted from Miri's tests/pass/tree_borrows/reserved.rs int_protected_read:
+; a protected Reserved pointer can observe a foreign read, but a later local
+; write is then forbidden.
+; RUN: not llubi --experimental-noalias --verbose < %s 2>&1 | FileCheck %s
+
+define void @foreign_read_then_local_write(ptr noalias %x, ptr %raw) {
+ %v = load i32, ptr %raw
+ store i32 1, ptr %x
+ ret void
+}
+
+define void @main() {
+ %a = alloca i32
+ store i32 0, ptr %a
+ call void @foreign_read_then_local_write(ptr %a, ptr %a)
+ ret void
+}
+
+; CHECK: Entering function: main
+; CHECK-NEXT: %a = alloca i32, align 4 => ptr 0x8 [a]
+; CHECK-NEXT: store i32 0, ptr %a, align 4
+; CHECK-NEXT: Entering function: foreign_read_then_local_write
+; CHECK-NEXT: ptr %x = ptr 0x8 [a]
+; CHECK-NEXT: ptr %raw = ptr 0x8 [a]
+; CHECK-NEXT: NoAlias: created protector node #1 for 'a' based on raw/root
+; CHECK-NEXT: NoAlias: node #1 foreign read through raw/root on 'a' bytes [0, 4): Reserved -> ReservedF
+; CHECK-NEXT: NoAlias: read through raw/root on 'a' bytes [0, 4) checked 1 active noalias protector
+; CHECK-NEXT: %v = load i32, ptr %raw, align 4 => i32 0
+; CHECK-NEXT: NoAlias: noalias violation: write through node #1 on 'a' bytes [0, 4) is local to protected node #1, but that protector is in ReservedF state
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0 store i32 1, ptr %x, align 4 at @foreign_read_then_local_write
+; CHECK-NEXT: #1 call void @foreign_read_then_local_write(ptr %a, ptr %a) at @main
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through node #1 on 'a' bytes [0, 4) is local to protected node #1, but that protector is in ReservedF state
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/tools/llubi/lib/Context.cpp b/llvm/tools/llubi/lib/Context.cpp
index cef55b53e0988..bfc5e8b91e2af 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 {
@@ -444,6 +447,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].
@@ -494,6 +501,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;
@@ -518,7 +541,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);
}
@@ -599,7 +624,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;
@@ -631,6 +657,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()) {
@@ -645,19 +680,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 {
llvm_unreachable("Unsupported scalar type.");
}
@@ -1014,6 +1051,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();
@@ -1225,4 +1263,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 097f0d1e0ab8b..cf10d52e67f3b 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();
@@ -233,6 +237,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
@@ -287,6 +292,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,
@@ -300,6 +348,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;
@@ -337,6 +423,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; }
@@ -347,6 +434,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;
@@ -434,6 +522,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 0cbb38073f652..5bc53de09362a 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 a1c8e49126d02..9cf2c13fbb36e 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 425215b4aee2e..f77d794727780 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -913,8 +913,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) {
@@ -2163,8 +2163,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);
}
}
@@ -2807,6 +2806,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.");
@@ -2847,6 +2847,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 77e428813a832..02ccf05e5bf19 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;
@@ -69,6 +71,7 @@ struct Byte {
ConcreteMask |= Mask;
Value = (Value & ~Mask) | (Val & Mask);
TagMask &= ~Mask;
+ NoAliasMask &= ~Mask;
}
void writeTagBits(uint8_t Mask, uint8_t Tag) {
@@ -79,25 +82,50 @@ struct Byte {
TagValue = (TagValue & ~Mask) | (Tag & Mask);
}
+ void writeNoAliasBits(uint8_t Mask, uint64_t NodeID) {
+ assert((ConcreteMask & Mask) == Mask &&
+ "Please ensure pointer bits are concrete before calling "
+ "writeNoAliasBits.");
+ if (!NodeID) {
+ NoAliasMask &= ~Mask;
+ return;
+ }
+ if (NoAliasMask && NoAliasNode != NodeID)
+ NoAliasMask = 0;
+ NoAliasNode = NodeID;
+ NoAliasMask |= Mask;
+ }
+
/// Returns a logical byte that is part of two adjacent bytes.
/// Example with ShAmt = 5:
/// | Low | High |
/// LSB | 0 1 0 1 0 1 0 1 | 0 0 0 0 1 1 1 1 | MSB
/// Result = | 1 0 1 0 0 0 0 1 |
static Byte fshr(const Byte &Low, const Byte &High, uint32_t ShAmt) {
+ uint16_t NoAliasMask = Low.NoAliasMask | (High.NoAliasMask << 8);
+ uint64_t NoAliasNode = Low.NoAliasNode;
+ if (Low.NoAliasMask && High.NoAliasMask &&
+ Low.NoAliasNode != High.NoAliasNode)
+ NoAliasMask = 0;
+ else if (!Low.NoAliasMask)
+ NoAliasNode = High.NoAliasNode;
return Byte{
static_cast<uint8_t>((Low.ConcreteMask | (High.ConcreteMask << 8)) >>
ShAmt),
static_cast<uint8_t>((Low.Value | (High.Value << 8)) >> ShAmt),
static_cast<uint8_t>((Low.TagMask | (High.TagMask << 8)) >> ShAmt),
- static_cast<uint8_t>((Low.TagValue | (High.TagValue << 8)) >> ShAmt)};
+ static_cast<uint8_t>((Low.TagValue | (High.TagValue << 8)) >> ShAmt),
+ static_cast<uint8_t>(NoAliasMask >> ShAmt),
+ NoAliasNode};
}
Byte lshr(uint8_t Shift) const {
return Byte{static_cast<uint8_t>(ConcreteMask >> Shift),
static_cast<uint8_t>(Value >> Shift),
static_cast<uint8_t>(TagMask >> Shift),
- static_cast<uint8_t>(TagValue >> Shift)};
+ static_cast<uint8_t>(TagValue >> Shift),
+ static_cast<uint8_t>(NoAliasMask >> Shift),
+ NoAliasNode};
}
};
@@ -199,6 +227,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)
@@ -207,17 +238,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; }
};
// Value representation for actual values of LLVM values.
diff --git a/llvm/tools/llubi/llubi.cpp b/llvm/tools/llubi/llubi.cpp
index fd0e8e8f55ace..16887d30590e2 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));
@@ -192,6 +197,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) {
@@ -254,6 +263,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 a7b650af4f187b0f919f61aee45175b42e4687b5 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/2] [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 aa946d0f11e84..173a9aaa4c259 100644
--- a/llvm/test/tools/llubi/noalias_outside_range.ll
+++ b/llvm/test/tools/llubi/noalias_outside_range.ll
@@ -36,21 +36,17 @@ define void @main() {
; CHECK-NEXT: NoAlias: write through raw/root on 'data' bytes [1, 2) checked 1 active noalias protector
; CHECK-NEXT: store i8 42, ptr %raw1, align 1
; CHECK-NEXT: %x2 = getelementptr i8, ptr %x, i64 2 => ptr 0xA [data + 2]
-; CHECK-NEXT: NoAlias: node #1 foreign read through raw/root on 'data' bytes [2, 3): Reserved -> ReservedF
-; CHECK-NEXT: NoAlias: read through raw/root on 'data' bytes [2, 3) checked 1 active noalias protector
+; CHECK-NEXT: NoAlias: node #1 local read through node #1 on 'data' bytes [2, 3): Reserved -> ReservedL
+; CHECK-NEXT: NoAlias: read through node #1 on 'data' bytes [2, 3) checked 1 active noalias protector
; CHECK-NEXT: %v2 = load i8, ptr %x2, align 1 => i8 62
; CHECK-NEXT: %x3 = getelementptr i8, ptr %x, i64 3 => ptr 0xB [data + 3]
-; CHECK-NEXT: NoAlias: node #1 foreign read through raw/root on 'data' bytes [3, 4): Reserved -> ReservedF
-; CHECK-NEXT: NoAlias: read through raw/root on 'data' bytes [3, 4) checked 1 active noalias protector
+; CHECK-NEXT: NoAlias: node #1 local read through node #1 on 'data' bytes [3, 4): Reserved -> ReservedL
+; CHECK-NEXT: NoAlias: read through node #1 on 'data' bytes [3, 4) checked 1 active noalias protector
; CHECK-NEXT: %v3 = load i8, ptr %x3, align 1 => i8 -117
; CHECK-NEXT: %raw3 = getelementptr i8, ptr %raw, i64 3 => ptr 0xB [data + 3]
-; CHECK-NEXT: NoAlias: node #1 foreign write through raw/root on 'data' bytes [3, 4): ReservedF -> Disabled
-; CHECK-NEXT: NoAlias: write through raw/root on 'data' bytes [3, 4) checked 1 active noalias protector
-; CHECK-NEXT: store i8 42, ptr %raw3, align 1
-; CHECK-NEXT: ret void
-; CHECK-NEXT: NoAlias: ended protector node #1
-; CHECK-NEXT: NoAlias: erased inactive protector node #1
-; CHECK-NEXT: Exiting function: stuff
-; CHECK-NEXT: call void @stuff(ptr %raw, ptr %raw)
-; CHECK-NEXT: ret void
-; CHECK-NEXT: Exiting function: main
+; CHECK-NEXT: NoAlias: noalias violation: write through raw/root on 'data' bytes [3, 4) is foreign to protected node #1, but that protector is in ReservedL state
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0 store i8 42, ptr %raw3, align 1 at @stuff
+; CHECK-NEXT: #1 call void @stuff(ptr %raw, ptr %raw) at @main
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through raw/root on 'data' bytes [3, 4) is foreign to protected node #1, but that protector is in ReservedL state
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/noalias_overlapping_ranges.ll b/llvm/test/tools/llubi/noalias_overlapping_ranges.ll
index 068e4a9136ee9..c9e4d8815fb62 100644
--- a/llvm/test/tools/llubi/noalias_overlapping_ranges.ll
+++ b/llvm/test/tools/llubi/noalias_overlapping_ranges.ll
@@ -26,21 +26,14 @@ define void @main() {
; CHECK-NEXT: NoAlias: created protector node #1 for 'array' based on raw/root
; CHECK-NEXT: NoAlias: created protector node #2 for 'array' based on raw/root
; CHECK-NEXT: %a1 = getelementptr [4 x i32], ptr %a, i64 0, i64 1 => ptr 0xC [array + 4]
-; CHECK-NEXT: NoAlias: node #1 foreign write through raw/root on 'array' bytes [4, 8): Reserved -> Disabled
-; CHECK-NEXT: NoAlias: node #2 foreign write through raw/root on 'array' bytes [4, 8): Reserved -> Disabled
-; CHECK-NEXT: NoAlias: write through raw/root on 'array' bytes [4, 8) checked 2 active noalias protectors
+; CHECK-NEXT: NoAlias: node #1 local write through node #1 on 'array' bytes [4, 8): Reserved -> Unique
+; CHECK-NEXT: NoAlias: node #2 foreign write through node #1 on 'array' bytes [4, 8): Reserved -> Disabled
+; CHECK-NEXT: NoAlias: write through node #1 on 'array' bytes [4, 8) checked 2 active noalias protectors
; CHECK-NEXT: store i32 5, ptr %a1, align 4
; CHECK-NEXT: %b1 = getelementptr [4 x i32], ptr %b, i64 0, i64 1 => ptr 0xC [array + 4]
-; CHECK-NEXT: NoAlias: node #1 foreign write through raw/root on 'array' bytes [4, 8): Disabled -> Disabled
-; CHECK-NEXT: NoAlias: node #2 foreign write through raw/root on 'array' bytes [4, 8): Disabled -> Disabled
-; CHECK-NEXT: NoAlias: write through raw/root on 'array' bytes [4, 8) checked 2 active noalias protectors
-; CHECK-NEXT: store i32 6, ptr %b1, align 4
-; CHECK-NEXT: ret void
-; CHECK-NEXT: NoAlias: ended protector node #1
-; CHECK-NEXT: NoAlias: erased inactive protector node #1
-; CHECK-NEXT: NoAlias: ended protector node #2
-; CHECK-NEXT: NoAlias: erased inactive protector node #2
-; CHECK-NEXT: Exiting function: write_overlap
-; CHECK-NEXT: call void @write_overlap(ptr %array, ptr %array)
-; CHECK-NEXT: ret void
-; CHECK-NEXT: Exiting function: main
+; CHECK-NEXT: NoAlias: noalias violation: write through node #2 on 'array' bytes [4, 8) is foreign to protected node #1, but that protector is in Unique state
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0 store i32 6, ptr %b1, align 4 at @write_overlap
+; CHECK-NEXT: #1 call void @write_overlap(ptr %array, ptr %array) at @main
+; CHECK-NEXT: Immediate UB detected: noalias violation: write through node #2 on 'array' bytes [4, 8) is foreign to protected node #1, but that protector is in Unique state
+; CHECK-NEXT: error: Execution of function 'main' failed.
More information about the llvm-commits
mailing list