[llvm] [llubi] Add support for captures (PR #201170)

Yingwei Zheng via llvm-commits llvm-commits at lists.llvm.org
Sat Aug 1 10:04:41 PDT 2026


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

>From feccfca3455eeca9ef2b601c4ebf996de40dc909 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Thu, 4 Jun 2026 01:31:10 +0800
Subject: [PATCH 1/4] [llubi] Add support for captures

---
 llvm/test/tools/llubi/captures.ll             | 120 ++++++++++++++++++
 ...dereferenceable_capture_read_provenance.ll |  24 ++++
 .../llubi/load_capture_read_provenance.ll     |  24 ++++
 .../tools/llubi/store_capture_provenance.ll   |  24 ++++
 .../tools/llubi/volatile_capture_address.ll   |  24 ++++
 llvm/test/tools/llubi/wildcard_provenance.ll  |  38 ++++++
 ...dcard_provenance_angelic_nondeterminism.ll |  31 +++++
 .../llubi/wildcard_provenance_gep_inbounds.ll |  29 +++++
 llvm/tools/llubi/lib/ExecutorBase.cpp         |  33 +++--
 llvm/tools/llubi/lib/ExecutorBase.h           |  20 +--
 llvm/tools/llubi/lib/Interpreter.cpp          |  75 ++++++++++-
 llvm/tools/llubi/lib/Library.cpp              |   6 +-
 llvm/tools/llubi/lib/Value.cpp                |   4 +-
 llvm/tools/llubi/lib/Value.h                  |  18 ++-
 14 files changed, 438 insertions(+), 32 deletions(-)
 create mode 100644 llvm/test/tools/llubi/captures.ll
 create mode 100644 llvm/test/tools/llubi/dereferenceable_capture_read_provenance.ll
 create mode 100644 llvm/test/tools/llubi/load_capture_read_provenance.ll
 create mode 100644 llvm/test/tools/llubi/store_capture_provenance.ll
 create mode 100644 llvm/test/tools/llubi/volatile_capture_address.ll
 create mode 100644 llvm/test/tools/llubi/wildcard_provenance.ll
 create mode 100644 llvm/test/tools/llubi/wildcard_provenance_angelic_nondeterminism.ll
 create mode 100644 llvm/test/tools/llubi/wildcard_provenance_gep_inbounds.ll

diff --git a/llvm/test/tools/llubi/captures.ll b/llvm/test/tools/llubi/captures.ll
new file mode 100644
index 0000000000000..4ac808c4248aa
--- /dev/null
+++ b/llvm/test/tools/llubi/captures.ll
@@ -0,0 +1,120 @@
+; 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 @write(ptr %p) {
+  store i8 1, ptr %p
+  ret void
+}
+
+define ptr @address_only(ptr captures(address) %p) {
+  ret ptr %p
+}
+
+define ptr @address_is_null_only(ptr captures(address_is_null) %p) {
+  ret ptr %p
+}
+
+define ptr @provenance_only(ptr captures(provenance) %p) {
+  ret ptr %p
+}
+
+define ptr @read_provenance_only(ptr captures(read_provenance) %p) {
+  ret ptr %p
+}
+
+ at g = global ptr null
+
+define ptr @ret_capture_read_provenance_other_capture_address(ptr captures(address, ret: read_provenance) %p) {
+  store ptr %p, ptr @g
+  %p2 = load ptr, ptr @g
+  ret ptr %p2
+}
+
+define void @main() {
+  %p = alloca i8
+  call void @write(ptr captures(none) %p)
+  %p_poison = call ptr @address_only(ptr poison)
+  %p_address_only = call ptr @address_only(ptr %p)
+  %p_address_is_null_only = call ptr @address_is_null_only(ptr %p)
+  %cmp1 = icmp eq ptr %p_address_only, %p_address_only
+  %cmp2 = icmp eq ptr %p_address_is_null_only, %p_address_is_null_only
+  %cmp3 = icmp eq ptr %p_address_is_null_only, null
+  %cmp4 = icmp eq ptr null, %p_address_is_null_only
+  %cmp5 = icmp eq ptr null, null
+
+  %p_read_provenance = call ptr @read_provenance_only(ptr %p)
+  %p_provenance = call ptr @provenance_only(ptr %p)
+
+  %p_read_provenance_2 = call ptr @read_provenance_only(ptr %p_provenance)
+  %p_none = call ptr @provenance_only(ptr %p_address_only)
+
+  %p_read_provenance2 = call ptr @ret_capture_read_provenance_other_capture_address(ptr %p)
+  %p_address_only2 = load ptr, ptr @g
+
+  call void @llvm.assume(i1 true) ["dereferenceable"(ptr %p_read_provenance, i32 1)]
+  %val = load i8, ptr %p_read_provenance
+  store i8 0, ptr %p_provenance
+
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %p = alloca i8, align 1 => ptr 0x11 [p]
+; CHECK-NEXT: Entering function: write
+; CHECK-NEXT:   ptr %p = ptr 0x11 [p]
+; CHECK-NEXT:   store i8 1, ptr %p, align 1
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: write
+; CHECK-NEXT:   call void @write(ptr captures(none) %p)
+; CHECK-NEXT: Entering function: address_only
+; CHECK-NEXT:   ptr %p = poison
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: Exiting function: address_only
+; CHECK-NEXT:   %p_poison = call ptr @address_only(ptr poison) => poison
+; CHECK-NEXT: Entering function: address_only
+; CHECK-NEXT:   ptr %p = ptr 0x11 [p]
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: Exiting function: address_only
+; CHECK-NEXT:   %p_address_only = call ptr @address_only(ptr %p) => ptr 0x11 [p address]
+; CHECK-NEXT: Entering function: address_is_null_only
+; CHECK-NEXT:   ptr %p = ptr 0x11 [p]
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: Exiting function: address_is_null_only
+; CHECK-NEXT:   %p_address_is_null_only = call ptr @address_is_null_only(ptr %p) => ptr 0x11 [p address_is_null]
+; CHECK-NEXT:   %cmp1 = icmp eq ptr %p_address_only, %p_address_only => T
+; CHECK-NEXT:   %cmp2 = icmp eq ptr %p_address_is_null_only, %p_address_is_null_only => poison
+; CHECK-NEXT:   %cmp3 = icmp eq ptr %p_address_is_null_only, null => F
+; CHECK-NEXT:   %cmp4 = icmp eq ptr null, %p_address_is_null_only => F
+; CHECK-NEXT:   %cmp5 = icmp eq ptr null, null => T
+; CHECK-NEXT: Entering function: read_provenance_only
+; CHECK-NEXT:   ptr %p = ptr 0x11 [p]
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: Exiting function: read_provenance_only
+; CHECK-NEXT:   %p_read_provenance = call ptr @read_provenance_only(ptr %p) => ptr 0x11 [p read_provenance]
+; CHECK-NEXT: Entering function: provenance_only
+; CHECK-NEXT:   ptr %p = ptr 0x11 [p]
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: Exiting function: provenance_only
+; CHECK-NEXT:   %p_provenance = call ptr @provenance_only(ptr %p) => ptr 0x11 [p provenance]
+; CHECK-NEXT: Entering function: read_provenance_only
+; CHECK-NEXT:   ptr %p = ptr 0x11 [p provenance]
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: Exiting function: read_provenance_only
+; CHECK-NEXT:   %p_read_provenance_2 = call ptr @read_provenance_only(ptr %p_provenance) => ptr 0x11 [p read_provenance]
+; CHECK-NEXT: Entering function: provenance_only
+; CHECK-NEXT:   ptr %p = ptr 0x11 [p address]
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: Exiting function: provenance_only
+; CHECK-NEXT:   %p_none = call ptr @provenance_only(ptr %p_address_only) => ptr 0x11 [p none]
+; CHECK-NEXT: Entering function: ret_capture_read_provenance_other_capture_address
+; CHECK-NEXT:   ptr %p = ptr 0x11 [p]
+; CHECK-NEXT:   store ptr %p, ptr @g, align 8
+; CHECK-NEXT:   %p2 = load ptr, ptr @g, align 8 => ptr 0x11 [p]
+; CHECK-NEXT:   ret ptr %p2
+; CHECK-NEXT: Exiting function: ret_capture_read_provenance_other_capture_address
+; CHECK-NEXT:   %p_read_provenance2 = call ptr @ret_capture_read_provenance_other_capture_address(ptr %p) => ptr 0x11 [p read_provenance]
+; CHECK-NEXT:   %p_address_only2 = load ptr, ptr @g, align 8 => ptr 0x11 [p address]
+; CHECK-NEXT:   call void @llvm.assume(i1 true) [ "dereferenceable"(ptr %p_read_provenance, i32 1) ]
+; CHECK-NEXT:   %val = load i8, ptr %p_read_provenance, align 1 => i8 1
+; CHECK-NEXT:   store i8 0, ptr %p_provenance, align 1
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/dereferenceable_capture_read_provenance.ll b/llvm/test/tools/llubi/dereferenceable_capture_read_provenance.ll
new file mode 100644
index 0000000000000..809756499b8c8
--- /dev/null
+++ b/llvm/test/tools/llubi/dereferenceable_capture_read_provenance.ll
@@ -0,0 +1,24 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
+
+define ptr @address_only(ptr captures(address) %p) {
+  ret ptr %p
+}
+
+ at g = global i8 0
+
+define void @main() {
+  %g_address_only = call ptr @address_only(ptr @g)
+  call void @llvm.assume(i1 true) ["dereferenceable"(ptr %g_address_only, i32 1)]
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT: Entering function: address_only
+; CHECK-NEXT:   ptr %p = ptr 0x8 [@g]
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: Exiting function: address_only
+; CHECK-NEXT:   %g_address_only = call ptr @address_only(ptr @g) => ptr 0x8 [@g address]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @llvm.assume(i1 true) [ "dereferenceable"(ptr %g_address_only, i32 1) ] at @main <stdin>:12
+; CHECK-NEXT: Immediate UB detected: The pointer ptr 0x8 [@g address] violates dereferenceable(1) assumption.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/load_capture_read_provenance.ll b/llvm/test/tools/llubi/load_capture_read_provenance.ll
new file mode 100644
index 0000000000000..ae4cf63a2dbb1
--- /dev/null
+++ b/llvm/test/tools/llubi/load_capture_read_provenance.ll
@@ -0,0 +1,24 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
+
+define ptr @address_only(ptr captures(address) %p) {
+  ret ptr %p
+}
+
+ at g = global i8 0
+
+define void @main() {
+  %g_address_only = call ptr @address_only(ptr @g)
+  %res = load i8, ptr %g_address_only
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT: Entering function: address_only
+; CHECK-NEXT:   ptr %p = ptr 0x8 [@g]
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: Exiting function: address_only
+; CHECK-NEXT:   %g_address_only = call ptr @address_only(ptr @g) => ptr 0x8 [@g address]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %res = load i8, ptr %g_address_only, align 1 at @main <stdin>:12
+; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/store_capture_provenance.ll b/llvm/test/tools/llubi/store_capture_provenance.ll
new file mode 100644
index 0000000000000..a5fd778d7a200
--- /dev/null
+++ b/llvm/test/tools/llubi/store_capture_provenance.ll
@@ -0,0 +1,24 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
+
+define ptr @address_read_provenance(ptr captures(address, read_provenance) %p) {
+  ret ptr %p
+}
+
+ at g = global i8 0
+
+define void @main() {
+  %g_address_read_provenance = call ptr @address_read_provenance(ptr @g)
+  store i8 1, ptr %g_address_read_provenance
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT: Entering function: address_read_provenance
+; CHECK-NEXT:   ptr %p = ptr 0x8 [@g]
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: Exiting function: address_read_provenance
+; CHECK-NEXT:   %g_address_read_provenance = call ptr @address_read_provenance(ptr @g) => ptr 0x8 [@g address, read_provenance]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i8 1, ptr %g_address_read_provenance, align 1 at @main <stdin>:12
+; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/volatile_capture_address.ll b/llvm/test/tools/llubi/volatile_capture_address.ll
new file mode 100644
index 0000000000000..28a84e1b893b8
--- /dev/null
+++ b/llvm/test/tools/llubi/volatile_capture_address.ll
@@ -0,0 +1,24 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
+
+define ptr @provenance_only(ptr captures(provenance) %p) {
+  ret ptr %p
+}
+
+ at g = global i8 0
+
+define void @main() {
+  %g_provenance_only = call ptr @provenance_only(ptr @g)
+  %res = load volatile i8, ptr %g_provenance_only
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT: Entering function: provenance_only
+; CHECK-NEXT:   ptr %p = ptr 0x8 [@g]
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: Exiting function: provenance_only
+; CHECK-NEXT:   %g_provenance_only = call ptr @provenance_only(ptr @g) => ptr 0x8 [@g provenance]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %res = load volatile i8, ptr %g_provenance_only, align 1 at @main <stdin>:12
+; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/wildcard_provenance.ll b/llvm/test/tools/llubi/wildcard_provenance.ll
new file mode 100644
index 0000000000000..806c67bb1edd6
--- /dev/null
+++ b/llvm/test/tools/llubi/wildcard_provenance.ll
@@ -0,0 +1,38 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --verbose < %s 2>&1 | FileCheck %s
+
+target datalayout = "e-p:64:64:64"
+
+define ptr @identity(ptr captures(provenance) %p) {
+  ret ptr %p
+}
+
+define void @main() {
+  %p = alloca i32
+  %p_read_provenance_only = call ptr @identity(ptr captures(read_provenance) %p)
+  %p_provenance_only = call ptr @identity(ptr captures(provenance) %p)
+  %addr1 = ptrtoint ptr %p_read_provenance_only to i64
+  %addr2 = ptrtoint ptr %p_provenance_only to i64
+  %p_wildcard = inttoptr i64 %addr1 to ptr
+  store i32 0, ptr %p_wildcard
+
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %p = alloca i32, align 4 => ptr 0x8 [p]
+; CHECK-NEXT: Entering function: identity
+; CHECK-NEXT:   ptr %p = ptr 0x8 [p]
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: Exiting function: identity
+; CHECK-NEXT:   %p_read_provenance_only = call ptr @identity(ptr captures(read_provenance) %p) => ptr 0x8 [p read_provenance]
+; CHECK-NEXT: Entering function: identity
+; CHECK-NEXT:   ptr %p = ptr 0x8 [p]
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: Exiting function: identity
+; CHECK-NEXT:   %p_provenance_only = call ptr @identity(ptr captures(provenance) %p) => ptr 0x8 [p provenance]
+; CHECK-NEXT:   %addr1 = ptrtoint ptr %p_read_provenance_only to i64 => i64 8
+; CHECK-NEXT:   %addr2 = ptrtoint ptr %p_provenance_only to i64 => i64 8
+; CHECK-NEXT:   %p_wildcard = inttoptr i64 %addr1 to ptr => ptr 0x8 [wildcard]
+; CHECK-NEXT:   store i32 0, ptr %p_wildcard, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/wildcard_provenance_angelic_nondeterminism.ll b/llvm/test/tools/llubi/wildcard_provenance_angelic_nondeterminism.ll
new file mode 100644
index 0000000000000..70c7747b2c2cd
--- /dev/null
+++ b/llvm/test/tools/llubi/wildcard_provenance_angelic_nondeterminism.ll
@@ -0,0 +1,31 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
+
+target datalayout = "e-p:64:64:64"
+
+define void @main() {
+  %p1 = alloca i32
+  %p2 = alloca i32
+  %addr1 = ptrtoint ptr %p1 to i64
+  %addr2 = ptrtoint ptr %p2 to i64
+  %diff = sub i64 %addr2, %addr1
+  %p = inttoptr i64 %addr1 to ptr
+  %p_off = getelementptr i8, ptr %p, i64 %diff
+  store i32 0, ptr %p_off
+  store i32 0, ptr %p
+
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %p1 = alloca i32, align 4 => ptr 0x8 [p1]
+; CHECK-NEXT:   %p2 = alloca i32, align 4 => ptr 0x10 [p2]
+; CHECK-NEXT:   %addr1 = ptrtoint ptr %p1 to i64 => i64 8
+; CHECK-NEXT:   %addr2 = ptrtoint ptr %p2 to i64 => i64 16
+; CHECK-NEXT:   %diff = sub i64 %addr2, %addr1 => i64 8
+; CHECK-NEXT:   %p = inttoptr i64 %addr1 to ptr => ptr 0x8 [wildcard]
+; CHECK-NEXT:   %p_off = getelementptr i8, ptr %p, i64 %diff => ptr 0x10 [wildcard]
+; CHECK-NEXT:   store i32 0, ptr %p_off, align 4
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i32 0, ptr %p, align 4 at @main <stdin>:15
+; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/wildcard_provenance_gep_inbounds.ll b/llvm/test/tools/llubi/wildcard_provenance_gep_inbounds.ll
new file mode 100644
index 0000000000000..9718b4743e46c
--- /dev/null
+++ b/llvm/test/tools/llubi/wildcard_provenance_gep_inbounds.ll
@@ -0,0 +1,29 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
+
+target datalayout = "e-p:64:64:64"
+
+define void @main() {
+  %p1 = alloca i32
+  %p2 = alloca i32
+  %addr1 = ptrtoint ptr %p1 to i64
+  %addr2 = ptrtoint ptr %p2 to i64
+  %diff = sub i64 %addr2, %addr1
+  %p = inttoptr i64 %addr1 to ptr
+  %p_off = getelementptr inbounds i8, ptr %p, i64 %diff
+  store i32 0, ptr %p_off
+
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %p1 = alloca i32, align 4 => ptr 0x8 [p1]
+; CHECK-NEXT:   %p2 = alloca i32, align 4 => ptr 0x10 [p2]
+; CHECK-NEXT:   %addr1 = ptrtoint ptr %p1 to i64 => i64 8
+; CHECK-NEXT:   %addr2 = ptrtoint ptr %p2 to i64 => i64 16
+; CHECK-NEXT:   %diff = sub i64 %addr2, %addr1 => i64 8
+; CHECK-NEXT:   %p = inttoptr i64 %addr1 to ptr => ptr 0x8 [wildcard]
+; CHECK-NEXT:   %p_off = getelementptr inbounds i8, ptr %p, i64 %diff => poison
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i32 0, ptr %p_off, align 4 at @main <stdin>:14
+; CHECK-NEXT: Immediate UB detected: Invalid memory access with a poison pointer.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/tools/llubi/lib/ExecutorBase.cpp b/llvm/tools/llubi/lib/ExecutorBase.cpp
index 0cbb38073f652..392abb2511e88 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.cpp
+++ b/llvm/tools/llubi/lib/ExecutorBase.cpp
@@ -15,9 +15,11 @@
 namespace llvm::ubi {
 Frame::Frame(Function &F, CallBase *CallSite, Frame *LastFrame,
              ArrayRef<AnyValue> Args, AnyValue &RetVal,
-             const TargetLibraryInfoImpl &TLIImpl)
+             const TargetLibraryInfoImpl &TLIImpl,
+             CapturedProvenanceList CapturedProvenances)
     : Func(F), LastFrame(LastFrame), CallSite(CallSite), Args(Args),
-      RetVal(RetVal), TLI(TLIImpl, &F) {
+      RetVal(RetVal), TLI(TLIImpl, &F),
+      CapturedProvenances(std::move(CapturedProvenances)) {
   assert((Args.size() == F.arg_size() ||
           (F.isVarArg() && Args.size() >= F.arg_size())) &&
          "Expected enough arguments to call the function.");
@@ -55,12 +57,19 @@ void ExecutorBase::reportErrorString(StringRef Msg) {
 
 std::pair<MemoryObject *, uint64_t>
 ExecutorBase::verifyMemAccess(const Pointer &Ptr, uint64_t AccessSize,
-                              Align Alignment, bool IsStore) {
-  auto *MO = Ctx.checkProvenance(Ptr, [](const Provenance &) {
-    // TODO: check provenance
-    // TODO: check inrange(S, E)
-    return true;
-  });
+                              Align Alignment, bool IsStore, bool IsVolatile) {
+  auto *MO =
+      Ctx.checkProvenance(Ptr, [IsStore, IsVolatile](const Provenance &Prov) {
+        CaptureComponents CC = Prov.capability();
+        if (IsVolatile &&
+            (CC & CaptureComponents::Address) != CaptureComponents::Address)
+          return false;
+        if (IsStore ? !capturesFullProvenance(CC) : !capturesAnyProvenance(CC))
+          return false;
+
+        // TODO: check inrange(S, E)
+        return true;
+      });
   if (!MO) {
     reportImmediateUB()
         << "Invalid memory access via a pointer with nullary provenance.";
@@ -116,7 +125,7 @@ ExecutorBase::verifyMemAccess(const Pointer &Ptr, uint64_t AccessSize,
 }
 
 AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
-                            bool NoUndef) {
+                            bool NoUndef, bool IsVolatile) {
   if (Ptr.isPoison()) {
     reportImmediateUB() << "Invalid memory access with a poison pointer.";
     return AnyValue::getPoisonValue(Ctx, ValTy);
@@ -124,7 +133,7 @@ AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
   auto &PtrVal = Ptr.asPointer();
   if (auto [MO, Offset] = verifyMemAccess(
           PtrVal, Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
-          /*IsStore=*/false);
+          /*IsStore=*/false, IsVolatile);
       MO) {
     bool ContainsUndefinedBits = false;
     AnyValue Res = Ctx.load(*MO, Offset, ValTy,
@@ -137,7 +146,7 @@ AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
 }
 
 void ExecutorBase::store(const AnyValue &Ptr, Align Alignment,
-                         const AnyValue &Val, Type *ValTy) {
+                         const AnyValue &Val, Type *ValTy, bool IsVolatile) {
   if (Ptr.isPoison()) {
     reportImmediateUB() << "Invalid memory access with a poison pointer.";
     return;
@@ -145,7 +154,7 @@ void ExecutorBase::store(const AnyValue &Ptr, Align Alignment,
   auto &PtrVal = Ptr.asPointer();
   if (auto [MO, Offset] = verifyMemAccess(
           PtrVal, Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
-          /*IsStore=*/true);
+          /*IsStore=*/true, IsVolatile);
       MO)
     Ctx.store(*MO, Offset, Val, ValTy);
 }
diff --git a/llvm/tools/llubi/lib/ExecutorBase.h b/llvm/tools/llubi/lib/ExecutorBase.h
index ff8025d189615..b556b35180182 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.h
+++ b/llvm/tools/llubi/lib/ExecutorBase.h
@@ -60,6 +60,10 @@ struct Frame {
   SmallVector<IntrusiveRefCntPtr<MemoryObject>> Allocas;
   // Values of arguments and executed instructions in this function.
   DenseMap<Value *, AnyValue> ValueMap;
+  // The provenances to be updated after the function returns or unwinds.
+  using CapturedProvenanceList =
+      SmallVector<std::pair<IntrusiveRefCntPtr<Provenance>, CaptureInfo>>;
+  CapturedProvenanceList CapturedProvenances;
 
   // Reserved for in-flight subroutines.
   Function *ResolvedCallee = nullptr;
@@ -71,7 +75,8 @@ struct Frame {
 
   Frame(Function &F, CallBase *CallSite, Frame *LastFrame,
         ArrayRef<AnyValue> Args, AnyValue &RetVal,
-        const TargetLibraryInfoImpl &TLIImpl);
+        const TargetLibraryInfoImpl &TLIImpl,
+        CapturedProvenanceList CapturedProvenance);
 };
 
 enum class DiagnosticKind {
@@ -104,15 +109,14 @@ class ExecutorBase {
 
   /// Check if the upcoming memory access is valid. Returns the resolved memory
   /// object and offset if it is valid.
-  std::pair<MemoryObject *, uint64_t> verifyMemAccess(const Pointer &Ptr,
-                                                      uint64_t AccessSize,
-                                                      Align Alignment,
-                                                      bool IsStore);
+  std::pair<MemoryObject *, uint64_t>
+  verifyMemAccess(const Pointer &Ptr, uint64_t AccessSize, Align Alignment,
+                  bool IsStore, bool IsVolatile);
 
-  AnyValue load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
-                bool NoUndef);
+  AnyValue load(const AnyValue &Ptr, Align Alignment, Type *ValTy, bool NoUndef,
+                bool IsVolatile);
   void store(const AnyValue &Ptr, Align Alignment, const AnyValue &Val,
-             Type *ValTy);
+             Type *ValTy, bool IsVolatile);
 
   void requestProgramExit(ProgramExitInfo::ProgramExitKind Kind,
                           uint64_t ExitCode = 0);
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index aa2d903bfb5f1..3d113ef67aea2 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -107,8 +107,10 @@ static bool violatesDereferenceableBytesAttr(const AnyValue &V, uint64_t Bytes,
     return true;
   }
 
-  auto *MO = Ctx.checkProvenance(Ptr, [&](const Provenance &) {
-    // TODO: check read_provenance
+  auto *MO = Ctx.checkProvenance(Ptr, [&](const Provenance &Prov) {
+    CaptureComponents CC = Prov.capability();
+    if (!capturesAnyProvenance(CC))
+      return false;
     // TODO: check nofree for attributes/metadata.
     return true;
   });
@@ -924,7 +926,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
       : 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());
+                           RetVal, Ctx.getTLIImpl(),
+                           Frame::CapturedProvenanceList{});
   }
 
   void visitReturnInst(ReturnInst &RI) {
@@ -2092,6 +2095,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
 
     // Handle parameter attributes (Attributes from resolved callee should be
     // applied if available).
+    Frame::CapturedProvenanceList Captures;
     for (auto [I, Arg] : enumerate(CB.args())) {
       Type *ArgTy = Arg->getType();
       AnyValue &ArgVal = CalleeArgs[I];
@@ -2151,6 +2155,17 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
         }
       }
       handleAttributes(ArgTy, ArgVal, AttrsAtCallSite, AttrsAtCallee);
+
+      // Handle captures
+      if (ArgVal.isPointer() && !CB.isByValArgument(I)) {
+        CaptureInfo CI =
+            AttrsAtCallSite.getCaptureInfo() & AttrsAtCallee.getCaptureInfo();
+        if (CI != CaptureInfo::all()) {
+          auto NewProv = ArgVal.asPointer().provenance().clone();
+          Captures.emplace_back(NewProv, CI);
+          ArgVal = ArgVal.asPointer().getWithNewProvenance(std::move(NewProv));
+        }
+      }
     }
 
     CurrentFrame->ResolvedCallee = Callee;
@@ -2174,7 +2189,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
       AnyValue &RetVal = CurrentFrame->CalleeRetVal;
       CurrentFrame->State = FrameState::Pending;
       CallStack.emplace_back(*Callee, &CB, CurrentFrame, Args, RetVal,
-                             Ctx.getTLIImpl());
+                             Ctx.getTLIImpl(), std::move(Captures));
     }
   }
 
@@ -2541,6 +2556,33 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {
       if (LHS.isPoison() || RHS.isPoison())
         return AnyValue::poison();
+      if (LHS.isPointer()) {
+        auto &LHSPtr = LHS.asPointer();
+        auto &RHSPtr = RHS.asPointer();
+        unsigned AS = I.getOperand(0)->getType()->getPointerAddressSpace();
+        // Check provenance
+        bool LHSIsNull = LHSPtr.isNullPtr(AS, DL);
+        bool RHSIsNull = RHSPtr.isNullPtr(AS, DL);
+
+        auto IsValidCompare = [&](const Pointer &Ptr,
+                                  bool OtherIsNull) -> bool {
+          return Ctx.checkProvenance(
+              Ptr,
+              [&](const Provenance &Prov) {
+                CaptureComponents CC = Prov.capability();
+                CaptureComponents Mask = OtherIsNull
+                                             ? CaptureComponents::AddressIsNull
+                                             : CaptureComponents::Address;
+                return (CC & Mask) == Mask;
+              },
+              /*HasSideEffect=*/false);
+        };
+
+        if (!LHSIsNull && !IsValidCompare(LHSPtr, RHSIsNull))
+          return AnyValue::poison();
+        if (!RHSIsNull && !IsValidCompare(RHSPtr, LHSIsNull))
+          return AnyValue::poison();
+      }
       const APInt &LHSVal =
           LHS.isPointer() ? LHS.asPointer().address() : LHS.asInteger();
       const APInt &RHSVal =
@@ -2705,8 +2747,9 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitLoadInst(LoadInst &LI) {
-    auto RetVal = load(getValue(LI.getPointerOperand()), LI.getAlign(),
-                       LI.getType(), LI.hasMetadata(LLVMContext::MD_noundef));
+    auto RetVal =
+        load(getValue(LI.getPointerOperand()), LI.getAlign(), LI.getType(),
+             LI.hasMetadata(LLVMContext::MD_noundef), LI.isVolatile());
     // TODO: track volatile loads
     handleMetadata(LI.getType(), RetVal, LI);
     setResult(LI, std::move(RetVal));
@@ -2717,7 +2760,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     auto &Val = getValue(SI.getValueOperand());
     // TODO: track volatile stores
     // TODO: handle metadata
-    store(Ptr, SI.getAlign(), Val, SI.getValueOperand()->getType());
+    store(Ptr, SI.getAlign(), Val, SI.getValueOperand()->getType(),
+          SI.isVolatile());
     if (!hasProgramExited() && !Handler.onInstructionExecuted(SI, AnyValue()))
       setFailed();
   }
@@ -2860,6 +2904,23 @@ 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.");
+        // Apply captures attributes
+        for (auto &[Prov, CI] : Top.CapturedProvenances) {
+          if (Top.RetVal.isPointer()) {
+            auto &PtrVal = Top.RetVal.asPointer();
+            if (&PtrVal.provenance() == Prov.get()) {
+              CaptureComponents Capability = PtrVal.provenance().capability();
+              if ((Capability & CI.getRetComponents()) !=
+                  (Capability & CI.getOtherComponents())) {
+                // We have to create a copy of provenance.
+                auto NewProv = Prov->clone();
+                NewProv->captureCapability(CI.getRetComponents());
+                Top.RetVal = PtrVal.getWithNewProvenance(std::move(NewProv));
+              }
+            }
+          }
+          Prov->captureCapability(CI.getOtherComponents());
+        }
         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/Library.cpp b/llvm/tools/llubi/lib/Library.cpp
index c3452b8af92c0..d224b2e02041b 100644
--- a/llvm/tools/llubi/lib/Library.cpp
+++ b/llvm/tools/llubi/lib/Library.cpp
@@ -35,7 +35,8 @@ std::optional<std::string> Library::readStringFromMemory(const Pointer &Ptr) {
 
   while (true) {
     auto [MO, ValidOffset] = Executor.verifyMemAccess(
-        Ptr.getWithNewAddr(Address + Offset), 1, Align(1), /*IsStore=*/false);
+        Ptr.getWithNewAddr(Address + Offset), 1, Align(1), /*IsStore=*/false,
+        /*IsVolatile=*/false);
     if (!MO)
       return std::nullopt;
 
@@ -278,7 +279,8 @@ AnyValue Library::executePrintf(ArrayRef<AnyValue> Args) {
     case 'n': {
       OS.flush();
       Executor.store(Arg, Align(4), AnyValue(APInt(32, Output.size())),
-                     Type::getInt32Ty(Ctx.getContext()));
+                     Type::getInt32Ty(Ctx.getContext()),
+                     /*IsVolatile=*/false);
       break;
     }
     case 'p': {
diff --git a/llvm/tools/llubi/lib/Value.cpp b/llvm/tools/llubi/lib/Value.cpp
index 0993441abf22d..8a9a46a24759c 100644
--- a/llvm/tools/llubi/lib/Value.cpp
+++ b/llvm/tools/llubi/lib/Value.cpp
@@ -47,7 +47,9 @@ void Pointer::print(raw_ostream &OS) const {
   } else {
     OS << (Prov->isWildcard() ? "wildcard" : "nullary");
   }
-  // TODO: print provenance
+  CaptureComponents CC = Prov->capability();
+  if (CC != CaptureComponents::All)
+    OS << ' ' << CC;
   OS << "]";
 }
 
diff --git a/llvm/tools/llubi/lib/Value.h b/llvm/tools/llubi/lib/Value.h
index b0f90aab2f1b5..bcbe332f92220 100644
--- a/llvm/tools/llubi/lib/Value.h
+++ b/llvm/tools/llubi/lib/Value.h
@@ -14,6 +14,7 @@
 #include "llvm/ADT/IntrusiveRefCntPtr.h"
 #include "llvm/IR/DataLayout.h"
 #include "llvm/IR/Type.h"
+#include "llvm/Support/ModRef.h"
 #include "llvm/Support/raw_ostream.h"
 
 namespace llvm::ubi {
@@ -199,8 +200,9 @@ class Provenance : public RefCountedBase<Provenance> {
   // Null if it is concrete.
   IntrusiveRefCntPtr<WildcardProvenance> Wildcard;
 
+  CaptureComponents Capability;
+
   // TODO: modeling nofree
-  // TODO: modeling captures
   // TODO: modeling inrange(Start, End) attribute
 
   const APInt &getTag() const { return Tag; }
@@ -209,10 +211,22 @@ class Provenance : public RefCountedBase<Provenance> {
   friend class Context;
 
 public:
-  Provenance(IntrusiveRefCntPtr<MemoryObject> Obj) : Obj(std::move(Obj)) {}
+  Provenance(IntrusiveRefCntPtr<MemoryObject> Obj,
+             CaptureComponents Capability = CaptureComponents::All)
+      : Obj(std::move(Obj)), Capability(Capability) {}
   static IntrusiveRefCntPtr<Provenance> nullary();
+  IntrusiveRefCntPtr<Provenance> clone() const {
+    IntrusiveRefCntPtr<Provenance> Res =
+        makeIntrusiveRefCnt<Provenance>(Obj, Capability);
+    Res->Wildcard = Wildcard;
+    return Res;
+  }
+  void captureCapability(CaptureComponents CapturedMask) {
+    Capability &= CapturedMask;
+  }
   IntrusiveRefCntPtr<Provenance> getWithKnownMemoryObject(MemoryObject &Obj);
   MemoryObject *getMemoryObject() const { return Obj.get(); }
+  CaptureComponents capability() const { return Capability; }
   bool isWildcard() const { return Wildcard != nullptr; }
 };
 

>From 01d29fa9fc1cf0865076752132d434ca07ed4b16 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Thu, 30 Jul 2026 21:38:28 +0800
Subject: [PATCH 2/4] [llubi] Remove address capturing support

---
 llvm/test/tools/llubi/captures.ll             | 22 ++++--------
 ...dereferenceable_capture_read_provenance.ll |  4 +--
 .../llubi/load_capture_read_provenance.ll     |  2 +-
 .../tools/llubi/store_capture_provenance.ll   |  2 +-
 .../tools/llubi/volatile_capture_address.ll   | 24 -------------
 llvm/test/tools/llubi/wildcard_provenance.ll  |  2 +-
 llvm/tools/llubi/lib/ExecutorBase.cpp         | 30 +++++++---------
 llvm/tools/llubi/lib/ExecutorBase.h           | 13 +++----
 llvm/tools/llubi/lib/Interpreter.cpp          | 35 ++-----------------
 llvm/tools/llubi/lib/Library.cpp              |  6 ++--
 llvm/tools/llubi/lib/Value.cpp                |  2 +-
 llvm/tools/llubi/lib/Value.h                  |  4 ++-
 12 files changed, 40 insertions(+), 106 deletions(-)
 delete mode 100644 llvm/test/tools/llubi/volatile_capture_address.ll

diff --git a/llvm/test/tools/llubi/captures.ll b/llvm/test/tools/llubi/captures.ll
index 4ac808c4248aa..c1ea5e46ccdb3 100644
--- a/llvm/test/tools/llubi/captures.ll
+++ b/llvm/test/tools/llubi/captures.ll
@@ -36,11 +36,6 @@ define void @main() {
   %p_poison = call ptr @address_only(ptr poison)
   %p_address_only = call ptr @address_only(ptr %p)
   %p_address_is_null_only = call ptr @address_is_null_only(ptr %p)
-  %cmp1 = icmp eq ptr %p_address_only, %p_address_only
-  %cmp2 = icmp eq ptr %p_address_is_null_only, %p_address_is_null_only
-  %cmp3 = icmp eq ptr %p_address_is_null_only, null
-  %cmp4 = icmp eq ptr null, %p_address_is_null_only
-  %cmp5 = icmp eq ptr null, null
 
   %p_read_provenance = call ptr @read_provenance_only(ptr %p)
   %p_provenance = call ptr @provenance_only(ptr %p)
@@ -74,17 +69,12 @@ define void @main() {
 ; CHECK-NEXT:   ptr %p = ptr 0x11 [p]
 ; CHECK-NEXT:   ret ptr %p
 ; CHECK-NEXT: Exiting function: address_only
-; CHECK-NEXT:   %p_address_only = call ptr @address_only(ptr %p) => ptr 0x11 [p address]
+; CHECK-NEXT:   %p_address_only = call ptr @address_only(ptr %p) => ptr 0x11 [p none]
 ; CHECK-NEXT: Entering function: address_is_null_only
 ; CHECK-NEXT:   ptr %p = ptr 0x11 [p]
 ; CHECK-NEXT:   ret ptr %p
 ; CHECK-NEXT: Exiting function: address_is_null_only
-; CHECK-NEXT:   %p_address_is_null_only = call ptr @address_is_null_only(ptr %p) => ptr 0x11 [p address_is_null]
-; CHECK-NEXT:   %cmp1 = icmp eq ptr %p_address_only, %p_address_only => T
-; CHECK-NEXT:   %cmp2 = icmp eq ptr %p_address_is_null_only, %p_address_is_null_only => poison
-; CHECK-NEXT:   %cmp3 = icmp eq ptr %p_address_is_null_only, null => F
-; CHECK-NEXT:   %cmp4 = icmp eq ptr null, %p_address_is_null_only => F
-; CHECK-NEXT:   %cmp5 = icmp eq ptr null, null => T
+; CHECK-NEXT:   %p_address_is_null_only = call ptr @address_is_null_only(ptr %p) => ptr 0x11 [p none]
 ; CHECK-NEXT: Entering function: read_provenance_only
 ; CHECK-NEXT:   ptr %p = ptr 0x11 [p]
 ; CHECK-NEXT:   ret ptr %p
@@ -94,14 +84,14 @@ define void @main() {
 ; CHECK-NEXT:   ptr %p = ptr 0x11 [p]
 ; CHECK-NEXT:   ret ptr %p
 ; CHECK-NEXT: Exiting function: provenance_only
-; CHECK-NEXT:   %p_provenance = call ptr @provenance_only(ptr %p) => ptr 0x11 [p provenance]
+; CHECK-NEXT:   %p_provenance = call ptr @provenance_only(ptr %p) => ptr 0x11 [p]
 ; CHECK-NEXT: Entering function: read_provenance_only
-; CHECK-NEXT:   ptr %p = ptr 0x11 [p provenance]
+; CHECK-NEXT:   ptr %p = ptr 0x11 [p]
 ; CHECK-NEXT:   ret ptr %p
 ; CHECK-NEXT: Exiting function: read_provenance_only
 ; CHECK-NEXT:   %p_read_provenance_2 = call ptr @read_provenance_only(ptr %p_provenance) => ptr 0x11 [p read_provenance]
 ; CHECK-NEXT: Entering function: provenance_only
-; CHECK-NEXT:   ptr %p = ptr 0x11 [p address]
+; CHECK-NEXT:   ptr %p = ptr 0x11 [p none]
 ; CHECK-NEXT:   ret ptr %p
 ; CHECK-NEXT: Exiting function: provenance_only
 ; CHECK-NEXT:   %p_none = call ptr @provenance_only(ptr %p_address_only) => ptr 0x11 [p none]
@@ -112,7 +102,7 @@ define void @main() {
 ; CHECK-NEXT:   ret ptr %p2
 ; CHECK-NEXT: Exiting function: ret_capture_read_provenance_other_capture_address
 ; CHECK-NEXT:   %p_read_provenance2 = call ptr @ret_capture_read_provenance_other_capture_address(ptr %p) => ptr 0x11 [p read_provenance]
-; CHECK-NEXT:   %p_address_only2 = load ptr, ptr @g, align 8 => ptr 0x11 [p address]
+; CHECK-NEXT:   %p_address_only2 = load ptr, ptr @g, align 8 => ptr 0x11 [p none]
 ; CHECK-NEXT:   call void @llvm.assume(i1 true) [ "dereferenceable"(ptr %p_read_provenance, i32 1) ]
 ; CHECK-NEXT:   %val = load i8, ptr %p_read_provenance, align 1 => i8 1
 ; CHECK-NEXT:   store i8 0, ptr %p_provenance, align 1
diff --git a/llvm/test/tools/llubi/dereferenceable_capture_read_provenance.ll b/llvm/test/tools/llubi/dereferenceable_capture_read_provenance.ll
index 809756499b8c8..f859d86fe0a93 100644
--- a/llvm/test/tools/llubi/dereferenceable_capture_read_provenance.ll
+++ b/llvm/test/tools/llubi/dereferenceable_capture_read_provenance.ll
@@ -17,8 +17,8 @@ define void @main() {
 ; CHECK-NEXT:   ptr %p = ptr 0x8 [@g]
 ; CHECK-NEXT:   ret ptr %p
 ; CHECK-NEXT: Exiting function: address_only
-; CHECK-NEXT:   %g_address_only = call ptr @address_only(ptr @g) => ptr 0x8 [@g address]
+; CHECK-NEXT:   %g_address_only = call ptr @address_only(ptr @g) => ptr 0x8 [@g none]
 ; CHECK-NEXT: Stacktrace:
 ; CHECK-NEXT: #0   call void @llvm.assume(i1 true) [ "dereferenceable"(ptr %g_address_only, i32 1) ] at @main <stdin>:12
-; CHECK-NEXT: Immediate UB detected: The pointer ptr 0x8 [@g address] violates dereferenceable(1) assumption.
+; CHECK-NEXT: Immediate UB detected: The pointer ptr 0x8 [@g none] violates dereferenceable(1) assumption.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/load_capture_read_provenance.ll b/llvm/test/tools/llubi/load_capture_read_provenance.ll
index ae4cf63a2dbb1..a06f6c0098d32 100644
--- a/llvm/test/tools/llubi/load_capture_read_provenance.ll
+++ b/llvm/test/tools/llubi/load_capture_read_provenance.ll
@@ -17,7 +17,7 @@ define void @main() {
 ; CHECK-NEXT:   ptr %p = ptr 0x8 [@g]
 ; CHECK-NEXT:   ret ptr %p
 ; CHECK-NEXT: Exiting function: address_only
-; CHECK-NEXT:   %g_address_only = call ptr @address_only(ptr @g) => ptr 0x8 [@g address]
+; CHECK-NEXT:   %g_address_only = call ptr @address_only(ptr @g) => ptr 0x8 [@g none]
 ; CHECK-NEXT: Stacktrace:
 ; CHECK-NEXT: #0   %res = load i8, ptr %g_address_only, align 1 at @main <stdin>:12
 ; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
diff --git a/llvm/test/tools/llubi/store_capture_provenance.ll b/llvm/test/tools/llubi/store_capture_provenance.ll
index a5fd778d7a200..920742c9fe5ff 100644
--- a/llvm/test/tools/llubi/store_capture_provenance.ll
+++ b/llvm/test/tools/llubi/store_capture_provenance.ll
@@ -17,7 +17,7 @@ define void @main() {
 ; CHECK-NEXT:   ptr %p = ptr 0x8 [@g]
 ; CHECK-NEXT:   ret ptr %p
 ; CHECK-NEXT: Exiting function: address_read_provenance
-; CHECK-NEXT:   %g_address_read_provenance = call ptr @address_read_provenance(ptr @g) => ptr 0x8 [@g address, read_provenance]
+; CHECK-NEXT:   %g_address_read_provenance = call ptr @address_read_provenance(ptr @g) => ptr 0x8 [@g read_provenance]
 ; CHECK-NEXT: Stacktrace:
 ; CHECK-NEXT: #0   store i8 1, ptr %g_address_read_provenance, align 1 at @main <stdin>:12
 ; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
diff --git a/llvm/test/tools/llubi/volatile_capture_address.ll b/llvm/test/tools/llubi/volatile_capture_address.ll
deleted file mode 100644
index 28a84e1b893b8..0000000000000
--- a/llvm/test/tools/llubi/volatile_capture_address.ll
+++ /dev/null
@@ -1,24 +0,0 @@
-; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
-; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
-
-define ptr @provenance_only(ptr captures(provenance) %p) {
-  ret ptr %p
-}
-
- at g = global i8 0
-
-define void @main() {
-  %g_provenance_only = call ptr @provenance_only(ptr @g)
-  %res = load volatile i8, ptr %g_provenance_only
-  ret void
-}
-; CHECK: Entering function: main
-; CHECK-NEXT: Entering function: provenance_only
-; CHECK-NEXT:   ptr %p = ptr 0x8 [@g]
-; CHECK-NEXT:   ret ptr %p
-; CHECK-NEXT: Exiting function: provenance_only
-; CHECK-NEXT:   %g_provenance_only = call ptr @provenance_only(ptr @g) => ptr 0x8 [@g provenance]
-; CHECK-NEXT: Stacktrace:
-; CHECK-NEXT: #0   %res = load volatile i8, ptr %g_provenance_only, align 1 at @main <stdin>:12
-; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
-; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/wildcard_provenance.ll b/llvm/test/tools/llubi/wildcard_provenance.ll
index 806c67bb1edd6..dbeec0cd59d40 100644
--- a/llvm/test/tools/llubi/wildcard_provenance.ll
+++ b/llvm/test/tools/llubi/wildcard_provenance.ll
@@ -29,7 +29,7 @@ define void @main() {
 ; CHECK-NEXT:   ptr %p = ptr 0x8 [p]
 ; CHECK-NEXT:   ret ptr %p
 ; CHECK-NEXT: Exiting function: identity
-; CHECK-NEXT:   %p_provenance_only = call ptr @identity(ptr captures(provenance) %p) => ptr 0x8 [p provenance]
+; CHECK-NEXT:   %p_provenance_only = call ptr @identity(ptr captures(provenance) %p) => ptr 0x8 [p]
 ; CHECK-NEXT:   %addr1 = ptrtoint ptr %p_read_provenance_only to i64 => i64 8
 ; CHECK-NEXT:   %addr2 = ptrtoint ptr %p_provenance_only to i64 => i64 8
 ; CHECK-NEXT:   %p_wildcard = inttoptr i64 %addr1 to ptr => ptr 0x8 [wildcard]
diff --git a/llvm/tools/llubi/lib/ExecutorBase.cpp b/llvm/tools/llubi/lib/ExecutorBase.cpp
index 392abb2511e88..0ba34f0f11621 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.cpp
+++ b/llvm/tools/llubi/lib/ExecutorBase.cpp
@@ -57,19 +57,15 @@ void ExecutorBase::reportErrorString(StringRef Msg) {
 
 std::pair<MemoryObject *, uint64_t>
 ExecutorBase::verifyMemAccess(const Pointer &Ptr, uint64_t AccessSize,
-                              Align Alignment, bool IsStore, bool IsVolatile) {
-  auto *MO =
-      Ctx.checkProvenance(Ptr, [IsStore, IsVolatile](const Provenance &Prov) {
-        CaptureComponents CC = Prov.capability();
-        if (IsVolatile &&
-            (CC & CaptureComponents::Address) != CaptureComponents::Address)
-          return false;
-        if (IsStore ? !capturesFullProvenance(CC) : !capturesAnyProvenance(CC))
-          return false;
-
-        // TODO: check inrange(S, E)
-        return true;
-      });
+                              Align Alignment, bool IsStore) {
+  auto *MO = Ctx.checkProvenance(Ptr, [IsStore](const Provenance &Prov) {
+    CaptureComponents CC = Prov.capability();
+    if (IsStore ? !capturesFullProvenance(CC) : !capturesAnyProvenance(CC))
+      return false;
+
+    // TODO: check inrange(S, E)
+    return true;
+  });
   if (!MO) {
     reportImmediateUB()
         << "Invalid memory access via a pointer with nullary provenance.";
@@ -125,7 +121,7 @@ ExecutorBase::verifyMemAccess(const Pointer &Ptr, uint64_t AccessSize,
 }
 
 AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
-                            bool NoUndef, bool IsVolatile) {
+                            bool NoUndef) {
   if (Ptr.isPoison()) {
     reportImmediateUB() << "Invalid memory access with a poison pointer.";
     return AnyValue::getPoisonValue(Ctx, ValTy);
@@ -133,7 +129,7 @@ AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
   auto &PtrVal = Ptr.asPointer();
   if (auto [MO, Offset] = verifyMemAccess(
           PtrVal, Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
-          /*IsStore=*/false, IsVolatile);
+          /*IsStore=*/false);
       MO) {
     bool ContainsUndefinedBits = false;
     AnyValue Res = Ctx.load(*MO, Offset, ValTy,
@@ -146,7 +142,7 @@ AnyValue ExecutorBase::load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
 }
 
 void ExecutorBase::store(const AnyValue &Ptr, Align Alignment,
-                         const AnyValue &Val, Type *ValTy, bool IsVolatile) {
+                         const AnyValue &Val, Type *ValTy) {
   if (Ptr.isPoison()) {
     reportImmediateUB() << "Invalid memory access with a poison pointer.";
     return;
@@ -154,7 +150,7 @@ void ExecutorBase::store(const AnyValue &Ptr, Align Alignment,
   auto &PtrVal = Ptr.asPointer();
   if (auto [MO, Offset] = verifyMemAccess(
           PtrVal, Ctx.getEffectiveTypeStoreSize(ValTy), Alignment,
-          /*IsStore=*/true, IsVolatile);
+          /*IsStore=*/true);
       MO)
     Ctx.store(*MO, Offset, Val, ValTy);
 }
diff --git a/llvm/tools/llubi/lib/ExecutorBase.h b/llvm/tools/llubi/lib/ExecutorBase.h
index b556b35180182..07591bb4bfc1f 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.h
+++ b/llvm/tools/llubi/lib/ExecutorBase.h
@@ -109,14 +109,15 @@ class ExecutorBase {
 
   /// Check if the upcoming memory access is valid. Returns the resolved memory
   /// object and offset if it is valid.
-  std::pair<MemoryObject *, uint64_t>
-  verifyMemAccess(const Pointer &Ptr, uint64_t AccessSize, Align Alignment,
-                  bool IsStore, bool IsVolatile);
+  std::pair<MemoryObject *, uint64_t> verifyMemAccess(const Pointer &Ptr,
+                                                      uint64_t AccessSize,
+                                                      Align Alignment,
+                                                      bool IsStore);
 
-  AnyValue load(const AnyValue &Ptr, Align Alignment, Type *ValTy, bool NoUndef,
-                bool IsVolatile);
+  AnyValue load(const AnyValue &Ptr, Align Alignment, Type *ValTy,
+                bool NoUndef);
   void store(const AnyValue &Ptr, Align Alignment, const AnyValue &Val,
-             Type *ValTy, bool IsVolatile);
+             Type *ValTy);
 
   void requestProgramExit(ProgramExitInfo::ProgramExitKind Kind,
                           uint64_t ExitCode = 0);
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 3d113ef67aea2..07adb88a66071 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -2556,33 +2556,6 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {
       if (LHS.isPoison() || RHS.isPoison())
         return AnyValue::poison();
-      if (LHS.isPointer()) {
-        auto &LHSPtr = LHS.asPointer();
-        auto &RHSPtr = RHS.asPointer();
-        unsigned AS = I.getOperand(0)->getType()->getPointerAddressSpace();
-        // Check provenance
-        bool LHSIsNull = LHSPtr.isNullPtr(AS, DL);
-        bool RHSIsNull = RHSPtr.isNullPtr(AS, DL);
-
-        auto IsValidCompare = [&](const Pointer &Ptr,
-                                  bool OtherIsNull) -> bool {
-          return Ctx.checkProvenance(
-              Ptr,
-              [&](const Provenance &Prov) {
-                CaptureComponents CC = Prov.capability();
-                CaptureComponents Mask = OtherIsNull
-                                             ? CaptureComponents::AddressIsNull
-                                             : CaptureComponents::Address;
-                return (CC & Mask) == Mask;
-              },
-              /*HasSideEffect=*/false);
-        };
-
-        if (!LHSIsNull && !IsValidCompare(LHSPtr, RHSIsNull))
-          return AnyValue::poison();
-        if (!RHSIsNull && !IsValidCompare(RHSPtr, LHSIsNull))
-          return AnyValue::poison();
-      }
       const APInt &LHSVal =
           LHS.isPointer() ? LHS.asPointer().address() : LHS.asInteger();
       const APInt &RHSVal =
@@ -2747,9 +2720,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitLoadInst(LoadInst &LI) {
-    auto RetVal =
-        load(getValue(LI.getPointerOperand()), LI.getAlign(), LI.getType(),
-             LI.hasMetadata(LLVMContext::MD_noundef), LI.isVolatile());
+    auto RetVal = load(getValue(LI.getPointerOperand()), LI.getAlign(),
+                       LI.getType(), LI.hasMetadata(LLVMContext::MD_noundef));
     // TODO: track volatile loads
     handleMetadata(LI.getType(), RetVal, LI);
     setResult(LI, std::move(RetVal));
@@ -2760,8 +2732,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     auto &Val = getValue(SI.getValueOperand());
     // TODO: track volatile stores
     // TODO: handle metadata
-    store(Ptr, SI.getAlign(), Val, SI.getValueOperand()->getType(),
-          SI.isVolatile());
+    store(Ptr, SI.getAlign(), Val, SI.getValueOperand()->getType());
     if (!hasProgramExited() && !Handler.onInstructionExecuted(SI, AnyValue()))
       setFailed();
   }
diff --git a/llvm/tools/llubi/lib/Library.cpp b/llvm/tools/llubi/lib/Library.cpp
index d224b2e02041b..c3452b8af92c0 100644
--- a/llvm/tools/llubi/lib/Library.cpp
+++ b/llvm/tools/llubi/lib/Library.cpp
@@ -35,8 +35,7 @@ std::optional<std::string> Library::readStringFromMemory(const Pointer &Ptr) {
 
   while (true) {
     auto [MO, ValidOffset] = Executor.verifyMemAccess(
-        Ptr.getWithNewAddr(Address + Offset), 1, Align(1), /*IsStore=*/false,
-        /*IsVolatile=*/false);
+        Ptr.getWithNewAddr(Address + Offset), 1, Align(1), /*IsStore=*/false);
     if (!MO)
       return std::nullopt;
 
@@ -279,8 +278,7 @@ AnyValue Library::executePrintf(ArrayRef<AnyValue> Args) {
     case 'n': {
       OS.flush();
       Executor.store(Arg, Align(4), AnyValue(APInt(32, Output.size())),
-                     Type::getInt32Ty(Ctx.getContext()),
-                     /*IsVolatile=*/false);
+                     Type::getInt32Ty(Ctx.getContext()));
       break;
     }
     case 'p': {
diff --git a/llvm/tools/llubi/lib/Value.cpp b/llvm/tools/llubi/lib/Value.cpp
index 8a9a46a24759c..0ea36d9536bfe 100644
--- a/llvm/tools/llubi/lib/Value.cpp
+++ b/llvm/tools/llubi/lib/Value.cpp
@@ -48,7 +48,7 @@ void Pointer::print(raw_ostream &OS) const {
     OS << (Prov->isWildcard() ? "wildcard" : "nullary");
   }
   CaptureComponents CC = Prov->capability();
-  if (CC != CaptureComponents::All)
+  if (CC != CaptureComponents::Provenance)
     OS << ' ' << CC;
   OS << "]";
 }
diff --git a/llvm/tools/llubi/lib/Value.h b/llvm/tools/llubi/lib/Value.h
index bcbe332f92220..79fdca177ab29 100644
--- a/llvm/tools/llubi/lib/Value.h
+++ b/llvm/tools/llubi/lib/Value.h
@@ -200,6 +200,8 @@ class Provenance : public RefCountedBase<Provenance> {
   // Null if it is concrete.
   IntrusiveRefCntPtr<WildcardProvenance> Wildcard;
 
+  // Controls whether we can read/write to the memory object via this
+  // provenance. Address and AddressIsNull is not supported.
   CaptureComponents Capability;
 
   // TODO: modeling nofree
@@ -212,7 +214,7 @@ class Provenance : public RefCountedBase<Provenance> {
 
 public:
   Provenance(IntrusiveRefCntPtr<MemoryObject> Obj,
-             CaptureComponents Capability = CaptureComponents::All)
+             CaptureComponents Capability = CaptureComponents::Provenance)
       : Obj(std::move(Obj)), Capability(Capability) {}
   static IntrusiveRefCntPtr<Provenance> nullary();
   IntrusiveRefCntPtr<Provenance> clone() const {

>From 1b08cc8776551043aedd7250a6009a2cb60827ab Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Thu, 30 Jul 2026 22:28:16 +0800
Subject: [PATCH 3/4] [llubi] Add more tests.

---
 llvm/test/tools/llubi/captures_mixed1.ll  | 44 ++++++++++++++++
 llvm/test/tools/llubi/captures_mixed2.ll  | 45 +++++++++++++++++
 llvm/test/tools/llubi/captures_mixed3.ll  | 45 +++++++++++++++++
 llvm/test/tools/llubi/captures_mixed4.ll  | 51 +++++++++++++++++++
 llvm/test/tools/llubi/captures_mixed5.ll  | 50 +++++++++++++++++++
 llvm/test/tools/llubi/captures_mixed6.ll  | 61 +++++++++++++++++++++++
 llvm/test/tools/llubi/captures_nested1.ll | 39 +++++++++++++++
 llvm/test/tools/llubi/captures_nested2.ll | 45 +++++++++++++++++
 8 files changed, 380 insertions(+)
 create mode 100644 llvm/test/tools/llubi/captures_mixed1.ll
 create mode 100644 llvm/test/tools/llubi/captures_mixed2.ll
 create mode 100644 llvm/test/tools/llubi/captures_mixed3.ll
 create mode 100644 llvm/test/tools/llubi/captures_mixed4.ll
 create mode 100644 llvm/test/tools/llubi/captures_mixed5.ll
 create mode 100644 llvm/test/tools/llubi/captures_mixed6.ll
 create mode 100644 llvm/test/tools/llubi/captures_nested1.ll
 create mode 100644 llvm/test/tools/llubi/captures_nested2.ll

diff --git a/llvm/test/tools/llubi/captures_mixed1.ll b/llvm/test/tools/llubi/captures_mixed1.ll
new file mode 100644
index 0000000000000..48a95325beab2
--- /dev/null
+++ b/llvm/test/tools/llubi/captures_mixed1.ll
@@ -0,0 +1,44 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --verbose < %s 2>&1 | FileCheck %s
+
+target datalayout = "e-p:64:64:64"
+
+define ptr @mix_pointer(ptr %pa, ptr %pb) {
+  %ba = bitcast ptr %pa to <2 x b32>
+  %bb = bitcast ptr %pb to <2 x b32>
+  %bc = shufflevector <2 x b32> %ba, <2 x b32> %bb, <2 x i32> <i32 0, i32 3>
+  %pc = bitcast <2 x b32> %bc to ptr
+  ret ptr %pc
+}
+
+define ptr @identity(ptr %a) {
+  ret ptr %a
+}
+
+define void @main() {
+  %p1 = alloca i32
+  %p2 = call ptr @identity(ptr %p1)
+  %mixed = call ptr @mix_pointer(ptr %p1, ptr %p2)
+  store i32 0, ptr %mixed
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %p1 = alloca i32, align 4 => ptr 0x8 [p1]
+; CHECK-NEXT: Entering function: identity
+; CHECK-NEXT:   ptr %a = ptr 0x8 [p1]
+; CHECK-NEXT:   ret ptr %a
+; CHECK-NEXT: Exiting function: identity
+; CHECK-NEXT:   %p2 = call ptr @identity(ptr %p1) => ptr 0x8 [p1]
+; CHECK-NEXT: Entering function: mix_pointer
+; CHECK-NEXT:   ptr %pa = ptr 0x8 [p1]
+; CHECK-NEXT:   ptr %pb = ptr 0x8 [p1]
+; CHECK-NEXT:   %ba = bitcast ptr %pa to <2 x b32> => { b32 00001000(00111110) 00000000(11011100) 00000000(01000001) 00000000(11001011) , b32 00000000(11000101) 00000000(00110111) 00000000(11101000) 00000000(00101000)  }
+; CHECK-NEXT:   %bb = bitcast ptr %pb to <2 x b32> => { b32 00001000(00111110) 00000000(11011100) 00000000(01000001) 00000000(11001011) , b32 00000000(11000101) 00000000(00110111) 00000000(11101000) 00000000(00101000)  }
+; CHECK-NEXT:   %bc = shufflevector <2 x b32> %ba, <2 x b32> %bb, <2 x i32> <i32 0, i32 3> => { b32 00001000(00111110) 00000000(11011100) 00000000(01000001) 00000000(11001011) , b32 00000000(11000101) 00000000(00110111) 00000000(11101000) 00000000(00101000)  }
+; CHECK-NEXT:   %pc = bitcast <2 x b32> %bc to ptr => ptr 0x8 [p1]
+; CHECK-NEXT:   ret ptr %pc
+; CHECK-NEXT: Exiting function: mix_pointer
+; CHECK-NEXT:   %mixed = call ptr @mix_pointer(ptr %p1, ptr %p2) => ptr 0x8 [p1]
+; CHECK-NEXT:   store i32 0, ptr %mixed, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/captures_mixed2.ll b/llvm/test/tools/llubi/captures_mixed2.ll
new file mode 100644
index 0000000000000..fc8bf1c903ad1
--- /dev/null
+++ b/llvm/test/tools/llubi/captures_mixed2.ll
@@ -0,0 +1,45 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
+
+target datalayout = "e-p:64:64:64"
+
+define ptr @mix_pointer(ptr %pa, ptr %pb) {
+  %ba = bitcast ptr %pa to <2 x b32>
+  %bb = bitcast ptr %pb to <2 x b32>
+  %bc = shufflevector <2 x b32> %ba, <2 x b32> %bb, <2 x i32> <i32 0, i32 3>
+  %pc = bitcast <2 x b32> %bc to ptr
+  ret ptr %pc
+}
+
+define ptr @identity(ptr captures(ret: address, provenance) %a) {
+  ret ptr %a
+}
+
+define void @main() {
+  %p1 = alloca i32
+  %p2 = call ptr @identity(ptr %p1)
+  %mixed = call ptr @mix_pointer(ptr %p1, ptr %p2)
+  store i32 0, ptr %mixed
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %p1 = alloca i32, align 4 => ptr 0x8 [p1]
+; CHECK-NEXT: Entering function: identity
+; CHECK-NEXT:   ptr %a = ptr 0x8 [p1]
+; CHECK-NEXT:   ret ptr %a
+; CHECK-NEXT: Exiting function: identity
+; CHECK-NEXT:   %p2 = call ptr @identity(ptr %p1) => ptr 0x8 [p1]
+; CHECK-NEXT: Entering function: mix_pointer
+; CHECK-NEXT:   ptr %pa = ptr 0x8 [p1]
+; CHECK-NEXT:   ptr %pb = ptr 0x8 [p1]
+; CHECK-NEXT:   %ba = bitcast ptr %pa to <2 x b32> => { b32 00001000(00111110) 00000000(11011100) 00000000(01000001) 00000000(11001011) , b32 00000000(11000101) 00000000(00110111) 00000000(11101000) 00000000(00101000)  }
+; CHECK-NEXT:   %bb = bitcast ptr %pb to <2 x b32> => { b32 00001000(10001011) 00000000(11111001) 00000000(01000000) 00000000(00111110) , b32 00000000(01111100) 00000000(00111010) 00000000(11111101) 00000000(11111101)  }
+; CHECK-NEXT:   %bc = shufflevector <2 x b32> %ba, <2 x b32> %bb, <2 x i32> <i32 0, i32 3> => { b32 00001000(00111110) 00000000(11011100) 00000000(01000001) 00000000(11001011) , b32 00000000(01111100) 00000000(00111010) 00000000(11111101) 00000000(11111101)  }
+; CHECK-NEXT:   %pc = bitcast <2 x b32> %bc to ptr => ptr 0x8 [nullary]
+; CHECK-NEXT:   ret ptr %pc
+; CHECK-NEXT: Exiting function: mix_pointer
+; CHECK-NEXT:   %mixed = call ptr @mix_pointer(ptr %p1, ptr %p2) => ptr 0x8 [nullary]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i32 0, ptr %mixed, align 4 at @main <stdin>:22
+; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/captures_mixed3.ll b/llvm/test/tools/llubi/captures_mixed3.ll
new file mode 100644
index 0000000000000..03526d926a577
--- /dev/null
+++ b/llvm/test/tools/llubi/captures_mixed3.ll
@@ -0,0 +1,45 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
+
+target datalayout = "e-p:64:64:64"
+
+define ptr @mix_pointer(ptr %pa, ptr %pb) {
+  %ba = bitcast ptr %pa to <2 x b32>
+  %bb = bitcast ptr %pb to <2 x b32>
+  %bc = shufflevector <2 x b32> %ba, <2 x b32> %bb, <2 x i32> <i32 0, i32 3>
+  %pc = bitcast <2 x b32> %bc to ptr
+  ret ptr %pc
+}
+
+define ptr @identity(ptr captures(read_provenance) %a) {
+  ret ptr %a
+}
+
+define void @main() {
+  %p1 = alloca i32
+  %p2 = call ptr @identity(ptr %p1)
+  %mixed = call ptr @mix_pointer(ptr %p1, ptr %p2)
+  store i32 0, ptr %mixed
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %p1 = alloca i32, align 4 => ptr 0x8 [p1]
+; CHECK-NEXT: Entering function: identity
+; CHECK-NEXT:   ptr %a = ptr 0x8 [p1]
+; CHECK-NEXT:   ret ptr %a
+; CHECK-NEXT: Exiting function: identity
+; CHECK-NEXT:   %p2 = call ptr @identity(ptr %p1) => ptr 0x8 [p1 read_provenance]
+; CHECK-NEXT: Entering function: mix_pointer
+; CHECK-NEXT:   ptr %pa = ptr 0x8 [p1]
+; CHECK-NEXT:   ptr %pb = ptr 0x8 [p1 read_provenance]
+; CHECK-NEXT:   %ba = bitcast ptr %pa to <2 x b32> => { b32 00001000(00111110) 00000000(11011100) 00000000(01000001) 00000000(11001011) , b32 00000000(11000101) 00000000(00110111) 00000000(11101000) 00000000(00101000)  }
+; CHECK-NEXT:   %bb = bitcast ptr %pb to <2 x b32> => { b32 00001000(10001011) 00000000(11111001) 00000000(01000000) 00000000(00111110) , b32 00000000(01111100) 00000000(00111010) 00000000(11111101) 00000000(11111101)  }
+; CHECK-NEXT:   %bc = shufflevector <2 x b32> %ba, <2 x b32> %bb, <2 x i32> <i32 0, i32 3> => { b32 00001000(00111110) 00000000(11011100) 00000000(01000001) 00000000(11001011) , b32 00000000(01111100) 00000000(00111010) 00000000(11111101) 00000000(11111101)  }
+; CHECK-NEXT:   %pc = bitcast <2 x b32> %bc to ptr => ptr 0x8 [nullary]
+; CHECK-NEXT:   ret ptr %pc
+; CHECK-NEXT: Exiting function: mix_pointer
+; CHECK-NEXT:   %mixed = call ptr @mix_pointer(ptr %p1, ptr %p2) => ptr 0x8 [nullary]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i32 0, ptr %mixed, align 4 at @main <stdin>:22
+; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/captures_mixed4.ll b/llvm/test/tools/llubi/captures_mixed4.ll
new file mode 100644
index 0000000000000..da2e331b46218
--- /dev/null
+++ b/llvm/test/tools/llubi/captures_mixed4.ll
@@ -0,0 +1,51 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
+
+target datalayout = "e-p:64:64:64"
+
+define ptr @mix_pointer(ptr %pa, ptr %pb) {
+  %ba = bitcast ptr %pa to <2 x b32>
+  %bb = bitcast ptr %pb to <2 x b32>
+  %bc = shufflevector <2 x b32> %ba, <2 x b32> %bb, <2 x i32> <i32 0, i32 3>
+  %pc = bitcast <2 x b32> %bc to ptr
+  ret ptr %pc
+}
+
+ at g = global ptr null
+
+define void @captured_by_mem(ptr captures(provenance) %a) {
+  store ptr %a, ptr @g
+  ret void
+}
+
+define void @main() {
+  %p1 = alloca i32
+  call void @captured_by_mem(ptr %p1)
+  %p2 = load ptr, ptr @g
+  %mixed = call ptr @mix_pointer(ptr %p1, ptr %p2)
+  store i32 0, ptr %mixed
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %p1 = alloca i32, align 4 => ptr 0x14 [p1]
+; CHECK-NEXT: Entering function: captured_by_mem
+; CHECK-NEXT:   ptr %a = ptr 0x14 [p1]
+; CHECK-NEXT:   store ptr %a, ptr @g, align 8
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: captured_by_mem
+; CHECK-NEXT:   call void @captured_by_mem(ptr %p1)
+; CHECK-NEXT:   %p2 = load ptr, ptr @g, align 8 => ptr 0x14 [p1]
+; CHECK-NEXT: Entering function: mix_pointer
+; CHECK-NEXT:   ptr %pa = ptr 0x14 [p1]
+; CHECK-NEXT:   ptr %pb = ptr 0x14 [p1]
+; CHECK-NEXT:   %ba = bitcast ptr %pa to <2 x b32> => { b32 00010100(10001011) 00000000(11111001) 00000000(01000000) 00000000(00111110) , b32 00000000(01111100) 00000000(00111010) 00000000(11111101) 00000000(11111101)  }
+; CHECK-NEXT:   %bb = bitcast ptr %pb to <2 x b32> => { b32 00010100(00111110) 00000000(11011100) 00000000(01000001) 00000000(11001011) , b32 00000000(11000101) 00000000(00110111) 00000000(11101000) 00000000(00101000)  }
+; CHECK-NEXT:   %bc = shufflevector <2 x b32> %ba, <2 x b32> %bb, <2 x i32> <i32 0, i32 3> => { b32 00010100(10001011) 00000000(11111001) 00000000(01000000) 00000000(00111110) , b32 00000000(11000101) 00000000(00110111) 00000000(11101000) 00000000(00101000)  }
+; CHECK-NEXT:   %pc = bitcast <2 x b32> %bc to ptr => ptr 0x14 [nullary]
+; CHECK-NEXT:   ret ptr %pc
+; CHECK-NEXT: Exiting function: mix_pointer
+; CHECK-NEXT:   %mixed = call ptr @mix_pointer(ptr %p1, ptr %p2) => ptr 0x14 [nullary]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i32 0, ptr %mixed, align 4 at @main <stdin>:26
+; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/captures_mixed5.ll b/llvm/test/tools/llubi/captures_mixed5.ll
new file mode 100644
index 0000000000000..e73c1af9bea96
--- /dev/null
+++ b/llvm/test/tools/llubi/captures_mixed5.ll
@@ -0,0 +1,50 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --verbose < %s 2>&1 | FileCheck %s
+
+target datalayout = "e-p:64:64:64"
+
+define ptr @mix_pointer(ptr %pa, ptr %pb) {
+  %ba = bitcast ptr %pa to <2 x b32>
+  %bb = bitcast ptr %pb to <2 x b32>
+  %bc = shufflevector <2 x b32> %ba, <2 x b32> %bb, <2 x i32> <i32 0, i32 3>
+  %pc = bitcast <2 x b32> %bc to ptr
+  ret ptr %pc
+}
+
+ at g = global ptr null
+
+define void @captured_by_mem(ptr captures(address, provenance) %a) {
+  store ptr %a, ptr @g
+  ret void
+}
+
+define void @main() {
+  %p1 = alloca i32
+  call void @captured_by_mem(ptr %p1)
+  %p2 = load ptr, ptr @g
+  %mixed = call ptr @mix_pointer(ptr %p1, ptr %p2)
+  store i32 0, ptr %mixed
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %p1 = alloca i32, align 4 => ptr 0x14 [p1]
+; CHECK-NEXT: Entering function: captured_by_mem
+; CHECK-NEXT:   ptr %a = ptr 0x14 [p1]
+; CHECK-NEXT:   store ptr %a, ptr @g, align 8
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: captured_by_mem
+; CHECK-NEXT:   call void @captured_by_mem(ptr %p1)
+; CHECK-NEXT:   %p2 = load ptr, ptr @g, align 8 => ptr 0x14 [p1]
+; CHECK-NEXT: Entering function: mix_pointer
+; CHECK-NEXT:   ptr %pa = ptr 0x14 [p1]
+; CHECK-NEXT:   ptr %pb = ptr 0x14 [p1]
+; CHECK-NEXT:   %ba = bitcast ptr %pa to <2 x b32> => { b32 00010100(00111110) 00000000(11011100) 00000000(01000001) 00000000(11001011) , b32 00000000(11000101) 00000000(00110111) 00000000(11101000) 00000000(00101000)  }
+; CHECK-NEXT:   %bb = bitcast ptr %pb to <2 x b32> => { b32 00010100(00111110) 00000000(11011100) 00000000(01000001) 00000000(11001011) , b32 00000000(11000101) 00000000(00110111) 00000000(11101000) 00000000(00101000)  }
+; CHECK-NEXT:   %bc = shufflevector <2 x b32> %ba, <2 x b32> %bb, <2 x i32> <i32 0, i32 3> => { b32 00010100(00111110) 00000000(11011100) 00000000(01000001) 00000000(11001011) , b32 00000000(11000101) 00000000(00110111) 00000000(11101000) 00000000(00101000)  }
+; CHECK-NEXT:   %pc = bitcast <2 x b32> %bc to ptr => ptr 0x14 [p1]
+; CHECK-NEXT:   ret ptr %pc
+; CHECK-NEXT: Exiting function: mix_pointer
+; CHECK-NEXT:   %mixed = call ptr @mix_pointer(ptr %p1, ptr %p2) => ptr 0x14 [p1]
+; CHECK-NEXT:   store i32 0, ptr %mixed, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/captures_mixed6.ll b/llvm/test/tools/llubi/captures_mixed6.ll
new file mode 100644
index 0000000000000..8665f98fddb2d
--- /dev/null
+++ b/llvm/test/tools/llubi/captures_mixed6.ll
@@ -0,0 +1,61 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
+
+target datalayout = "e-p:64:64:64"
+
+define ptr @mix_pointer(ptr %pa, ptr %pb) {
+  %ba = bitcast ptr %pa to <2 x b32>
+  %bb = bitcast ptr %pb to <2 x b32>
+  %bc = shufflevector <2 x b32> %ba, <2 x b32> %bb, <2 x i32> <i32 0, i32 3>
+  %pc = bitcast <2 x b32> %bc to ptr
+  ret ptr %pc
+}
+
+ at g = global ptr null
+
+define ptr @capture_read_provenance(ptr captures(read_provenance) %a) {
+  ret ptr %a
+}
+
+define void @captured_by_mem(ptr captures(read_provenance) %a) {
+  store ptr %a, ptr @g
+  ret void
+}
+
+define void @main() {
+  %p1 = alloca i32
+  %p2 = call ptr @capture_read_provenance(ptr %p1)
+  call void @captured_by_mem(ptr %p2)
+  %p3 = load ptr, ptr @g
+  %mixed = call ptr @mix_pointer(ptr %p2, ptr %p3)
+  %v = load i32, ptr %mixed
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %p1 = alloca i32, align 4 => ptr 0x14 [p1]
+; CHECK-NEXT: Entering function: capture_read_provenance
+; CHECK-NEXT:   ptr %a = ptr 0x14 [p1]
+; CHECK-NEXT:   ret ptr %a
+; CHECK-NEXT: Exiting function: capture_read_provenance
+; CHECK-NEXT:   %p2 = call ptr @capture_read_provenance(ptr %p1) => ptr 0x14 [p1 read_provenance]
+; CHECK-NEXT: Entering function: captured_by_mem
+; CHECK-NEXT:   ptr %a = ptr 0x14 [p1 read_provenance]
+; CHECK-NEXT:   store ptr %a, ptr @g, align 8
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: captured_by_mem
+; CHECK-NEXT:   call void @captured_by_mem(ptr %p2)
+; CHECK-NEXT:   %p3 = load ptr, ptr @g, align 8 => ptr 0x14 [p1 read_provenance]
+; CHECK-NEXT: Entering function: mix_pointer
+; CHECK-NEXT:   ptr %pa = ptr 0x14 [p1 read_provenance]
+; CHECK-NEXT:   ptr %pb = ptr 0x14 [p1 read_provenance]
+; CHECK-NEXT:   %ba = bitcast ptr %pa to <2 x b32> => { b32 00010100(10001011) 00000000(11111001) 00000000(01000000) 00000000(00111110) , b32 00000000(01111100) 00000000(00111010) 00000000(11111101) 00000000(11111101)  }
+; CHECK-NEXT:   %bb = bitcast ptr %pb to <2 x b32> => { b32 00010100(00111110) 00000000(11011100) 00000000(01000001) 00000000(11001011) , b32 00000000(11000101) 00000000(00110111) 00000000(11101000) 00000000(00101000)  }
+; CHECK-NEXT:   %bc = shufflevector <2 x b32> %ba, <2 x b32> %bb, <2 x i32> <i32 0, i32 3> => { b32 00010100(10001011) 00000000(11111001) 00000000(01000000) 00000000(00111110) , b32 00000000(11000101) 00000000(00110111) 00000000(11101000) 00000000(00101000)  }
+; CHECK-NEXT:   %pc = bitcast <2 x b32> %bc to ptr => ptr 0x14 [nullary]
+; CHECK-NEXT:   ret ptr %pc
+; CHECK-NEXT: Exiting function: mix_pointer
+; CHECK-NEXT:   %mixed = call ptr @mix_pointer(ptr %p2, ptr %p3) => ptr 0x14 [nullary]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   %v = load i32, ptr %mixed, align 4 at @main <stdin>:31
+; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/captures_nested1.ll b/llvm/test/tools/llubi/captures_nested1.ll
new file mode 100644
index 0000000000000..fa7a60ceb58ab
--- /dev/null
+++ b/llvm/test/tools/llubi/captures_nested1.ll
@@ -0,0 +1,39 @@
+; 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 ptr @g(ptr captures(read_provenance) %a) {
+  store i32 0, ptr %a
+  ret ptr %a
+}
+
+define ptr @f(ptr captures(none) %a) {
+  store i32 0, ptr %a
+  %res = call ptr @g(ptr %a)
+  %v = load i32, ptr %res
+  ret ptr %res
+}
+
+define void @main() {
+  %p1 = alloca i32
+  %p2 = call ptr @f(ptr %p1)
+  %v = load i32, ptr %p2
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %p1 = alloca i32, align 4 => ptr 0x8 [p1]
+; CHECK-NEXT: Entering function: f
+; CHECK-NEXT:   ptr %a = ptr 0x8 [p1]
+; CHECK-NEXT:   store i32 0, ptr %a, align 4
+; CHECK-NEXT: Entering function: g
+; CHECK-NEXT:   ptr %a = ptr 0x8 [p1]
+; CHECK-NEXT:   store i32 0, ptr %a, align 4
+; CHECK-NEXT:   ret ptr %a
+; CHECK-NEXT: Exiting function: g
+; CHECK-NEXT:   %res = call ptr @g(ptr %a) => ptr 0x8 [p1 read_provenance]
+; CHECK-NEXT:   %v = load i32, ptr %res, align 4 => i32 0
+; CHECK-NEXT:   ret ptr %res
+; CHECK-NEXT: Exiting function: f
+; CHECK-NEXT:   %p2 = call ptr @f(ptr %p1) => ptr 0x8 [p1 read_provenance]
+; CHECK-NEXT:   %v = load i32, ptr %p2, align 4 => i32 0
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/captures_nested2.ll b/llvm/test/tools/llubi/captures_nested2.ll
new file mode 100644
index 0000000000000..0316f4cc018b3
--- /dev/null
+++ b/llvm/test/tools/llubi/captures_nested2.ll
@@ -0,0 +1,45 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --verbose < %s 2>&1 | FileCheck %s
+
+ at mem = global ptr null
+
+define void @g(ptr captures(read_provenance) %a) {
+  store i32 0, ptr %a
+  store ptr %a, ptr @mem
+  ret void
+}
+
+define ptr @f(ptr captures(none) %a) {
+  store i32 0, ptr %a
+  call void @g(ptr %a)
+  %res = load ptr, ptr @mem
+  %v = load i32, ptr %res
+  ret ptr %res
+}
+
+define void @main() {
+  %p1 = alloca i32
+  %p2 = call ptr @f(ptr %p1)
+  %v = load i32, ptr %p2
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %p1 = alloca i32, align 4 => ptr 0x14 [p1]
+; CHECK-NEXT: Entering function: f
+; CHECK-NEXT:   ptr %a = ptr 0x14 [p1]
+; CHECK-NEXT:   store i32 0, ptr %a, align 4
+; CHECK-NEXT: Entering function: g
+; CHECK-NEXT:   ptr %a = ptr 0x14 [p1]
+; CHECK-NEXT:   store i32 0, ptr %a, align 4
+; CHECK-NEXT:   store ptr %a, ptr @mem, align 8
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: g
+; CHECK-NEXT:   call void @g(ptr %a)
+; CHECK-NEXT:   %res = load ptr, ptr @mem, align 8 => ptr 0x14 [p1 read_provenance]
+; CHECK-NEXT:   %v = load i32, ptr %res, align 4 => i32 0
+; CHECK-NEXT:   ret ptr %res
+; CHECK-NEXT: Exiting function: f
+; CHECK-NEXT:   %p2 = call ptr @f(ptr %p1) => ptr 0x14 [p1 read_provenance]
+; CHECK-NEXT:   %v = load i32, ptr %p2, align 4 => i32 0
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main

>From 2f66a08c12ae2f93c762626bed5e0e1c303f2372 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Sun, 2 Aug 2026 01:04:20 +0800
Subject: [PATCH 4/4] [llubi] Add more tests and comments.

---
 llvm/test/tools/llubi/captures_mixed1.ll      |  2 ++
 llvm/test/tools/llubi/captures_mixed2.ll      |  5 ++-
 llvm/test/tools/llubi/captures_mixed3.ll      |  4 ++-
 llvm/test/tools/llubi/captures_mixed4.ll      |  4 ++-
 llvm/test/tools/llubi/captures_mixed5.ll      |  2 ++
 llvm/test/tools/llubi/captures_mixed6.ll      |  4 ++-
 llvm/test/tools/llubi/captures_nested1.ll     |  3 ++
 llvm/test/tools/llubi/captures_nested2.ll     |  4 +++
 .../tools/llubi/retonly_capture_provenance.ll | 34 +++++++++++++++++++
 .../tools/llubi/store_capture_provenance.ll   |  3 +-
 10 files changed, 60 insertions(+), 5 deletions(-)
 create mode 100644 llvm/test/tools/llubi/retonly_capture_provenance.ll

diff --git a/llvm/test/tools/llubi/captures_mixed1.ll b/llvm/test/tools/llubi/captures_mixed1.ll
index 48a95325beab2..1a318721b86fe 100644
--- a/llvm/test/tools/llubi/captures_mixed1.ll
+++ b/llvm/test/tools/llubi/captures_mixed1.ll
@@ -11,6 +11,8 @@ define ptr @mix_pointer(ptr %pa, ptr %pb) {
   ret ptr %pc
 }
 
+; Ensure it is identical to `captures(address, provenance)`.
+
 define ptr @identity(ptr %a) {
   ret ptr %a
 }
diff --git a/llvm/test/tools/llubi/captures_mixed2.ll b/llvm/test/tools/llubi/captures_mixed2.ll
index fc8bf1c903ad1..0f3d263af0550 100644
--- a/llvm/test/tools/llubi/captures_mixed2.ll
+++ b/llvm/test/tools/llubi/captures_mixed2.ll
@@ -11,6 +11,9 @@ define ptr @mix_pointer(ptr %pa, ptr %pb) {
   ret ptr %pc
 }
 
+; All components are captured through return value only. The returned pointer
+; should be identical to the input.
+
 define ptr @identity(ptr captures(ret: address, provenance) %a) {
   ret ptr %a
 }
@@ -40,6 +43,6 @@ define void @main() {
 ; CHECK-NEXT: Exiting function: mix_pointer
 ; CHECK-NEXT:   %mixed = call ptr @mix_pointer(ptr %p1, ptr %p2) => ptr 0x8 [nullary]
 ; CHECK-NEXT: Stacktrace:
-; CHECK-NEXT: #0   store i32 0, ptr %mixed, align 4 at @main <stdin>:22
+; CHECK-NEXT: #0   store i32 0, ptr %mixed, align 4 at @main <stdin>:25
 ; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/captures_mixed3.ll b/llvm/test/tools/llubi/captures_mixed3.ll
index 03526d926a577..7845e55942b6d 100644
--- a/llvm/test/tools/llubi/captures_mixed3.ll
+++ b/llvm/test/tools/llubi/captures_mixed3.ll
@@ -11,6 +11,8 @@ define ptr @mix_pointer(ptr %pa, ptr %pb) {
   ret ptr %pc
 }
 
+; The returned pointer only captures read_provenance. So it cannot be mixed with the input.
+
 define ptr @identity(ptr captures(read_provenance) %a) {
   ret ptr %a
 }
@@ -40,6 +42,6 @@ define void @main() {
 ; CHECK-NEXT: Exiting function: mix_pointer
 ; CHECK-NEXT:   %mixed = call ptr @mix_pointer(ptr %p1, ptr %p2) => ptr 0x8 [nullary]
 ; CHECK-NEXT: Stacktrace:
-; CHECK-NEXT: #0   store i32 0, ptr %mixed, align 4 at @main <stdin>:22
+; CHECK-NEXT: #0   store i32 0, ptr %mixed, align 4 at @main <stdin>:24
 ; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/captures_mixed4.ll b/llvm/test/tools/llubi/captures_mixed4.ll
index da2e331b46218..f0a5dd03e639f 100644
--- a/llvm/test/tools/llubi/captures_mixed4.ll
+++ b/llvm/test/tools/llubi/captures_mixed4.ll
@@ -11,6 +11,8 @@ define ptr @mix_pointer(ptr %pa, ptr %pb) {
   ret ptr %pc
 }
 
+; Only provenance is captured through the memory. So the stored pointer cannot be mixed with the input.
+
 @g = global ptr null
 
 define void @captured_by_mem(ptr captures(provenance) %a) {
@@ -46,6 +48,6 @@ define void @main() {
 ; CHECK-NEXT: Exiting function: mix_pointer
 ; CHECK-NEXT:   %mixed = call ptr @mix_pointer(ptr %p1, ptr %p2) => ptr 0x14 [nullary]
 ; CHECK-NEXT: Stacktrace:
-; CHECK-NEXT: #0   store i32 0, ptr %mixed, align 4 at @main <stdin>:26
+; CHECK-NEXT: #0   store i32 0, ptr %mixed, align 4 at @main <stdin>:28
 ; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/captures_mixed5.ll b/llvm/test/tools/llubi/captures_mixed5.ll
index e73c1af9bea96..043927006d4b3 100644
--- a/llvm/test/tools/llubi/captures_mixed5.ll
+++ b/llvm/test/tools/llubi/captures_mixed5.ll
@@ -11,6 +11,8 @@ define ptr @mix_pointer(ptr %pa, ptr %pb) {
   ret ptr %pc
 }
 
+; All components are captured through the memory. The stored pointer should be identical to the input.
+
 @g = global ptr null
 
 define void @captured_by_mem(ptr captures(address, provenance) %a) {
diff --git a/llvm/test/tools/llubi/captures_mixed6.ll b/llvm/test/tools/llubi/captures_mixed6.ll
index 8665f98fddb2d..45a0884843827 100644
--- a/llvm/test/tools/llubi/captures_mixed6.ll
+++ b/llvm/test/tools/llubi/captures_mixed6.ll
@@ -17,6 +17,8 @@ define ptr @capture_read_provenance(ptr captures(read_provenance) %a) {
   ret ptr %a
 }
 
+; The only one component (read_provenance) is captured through the memory. The stored pointer should be identical to the input.
+
 define void @captured_by_mem(ptr captures(read_provenance) %a) {
   store ptr %a, ptr @g
   ret void
@@ -56,6 +58,6 @@ define void @main() {
 ; CHECK-NEXT: Exiting function: mix_pointer
 ; CHECK-NEXT:   %mixed = call ptr @mix_pointer(ptr %p2, ptr %p3) => ptr 0x14 [nullary]
 ; CHECK-NEXT: Stacktrace:
-; CHECK-NEXT: #0   %v = load i32, ptr %mixed, align 4 at @main <stdin>:31
+; CHECK-NEXT: #0   %v = load i32, ptr %mixed, align 4 at @main <stdin>:33
 ; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/captures_nested1.ll b/llvm/test/tools/llubi/captures_nested1.ll
index fa7a60ceb58ab..618eb05dadd71 100644
--- a/llvm/test/tools/llubi/captures_nested1.ll
+++ b/llvm/test/tools/llubi/captures_nested1.ll
@@ -7,8 +7,10 @@ define ptr @g(ptr captures(read_provenance) %a) {
 }
 
 define ptr @f(ptr captures(none) %a) {
+  ; %a has full provenance here.
   store i32 0, ptr %a
   %res = call ptr @g(ptr %a)
+  ; %res has only read_provenance.
   %v = load i32, ptr %res
   ret ptr %res
 }
@@ -16,6 +18,7 @@ define ptr @f(ptr captures(none) %a) {
 define void @main() {
   %p1 = alloca i32
   %p2 = call ptr @f(ptr %p1)
+  ; %p2 has nullary provenance.
   %v = load i32, ptr %p2
   ret void
 }
diff --git a/llvm/test/tools/llubi/captures_nested2.ll b/llvm/test/tools/llubi/captures_nested2.ll
index 0316f4cc018b3..6a82f93d7242d 100644
--- a/llvm/test/tools/llubi/captures_nested2.ll
+++ b/llvm/test/tools/llubi/captures_nested2.ll
@@ -4,14 +4,17 @@
 @mem = global ptr null
 
 define void @g(ptr captures(read_provenance) %a) {
+  ; %a has full provenance here.
   store i32 0, ptr %a
   store ptr %a, ptr @mem
   ret void
 }
 
 define ptr @f(ptr captures(none) %a) {
+  ; %a has full provenance here.
   store i32 0, ptr %a
   call void @g(ptr %a)
+  ; %res has only read_provenance.
   %res = load ptr, ptr @mem
   %v = load i32, ptr %res
   ret ptr %res
@@ -20,6 +23,7 @@ define ptr @f(ptr captures(none) %a) {
 define void @main() {
   %p1 = alloca i32
   %p2 = call ptr @f(ptr %p1)
+  ; %p2 has nullary provenance.
   %v = load i32, ptr %p2
   ret void
 }
diff --git a/llvm/test/tools/llubi/retonly_capture_provenance.ll b/llvm/test/tools/llubi/retonly_capture_provenance.ll
new file mode 100644
index 0000000000000..4c3fca5d0901b
--- /dev/null
+++ b/llvm/test/tools/llubi/retonly_capture_provenance.ll
@@ -0,0 +1,34 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
+
+ at g = global ptr null
+
+define ptr @retonly_capture_provenance(ptr captures(ret: address, provenance) %p) {
+  store ptr %p, ptr @g
+  ret ptr %p
+}
+
+define void @main() {
+  %p = alloca i32
+  %p1 = call ptr @retonly_capture_provenance(ptr %p)
+  ; Provenance is captured via the returned pointer.
+  store i32 1, ptr %p1
+  ; Provenance is not captured in other ways.
+  %p2 = load ptr, ptr @g
+  store i32 1, ptr %p2
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %p = alloca i32, align 4 => ptr 0x14 [p]
+; CHECK-NEXT: Entering function: retonly_capture_provenance
+; CHECK-NEXT:   ptr %p = ptr 0x14 [p]
+; CHECK-NEXT:   store ptr %p, ptr @g, align 8
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: Exiting function: retonly_capture_provenance
+; CHECK-NEXT:   %p1 = call ptr @retonly_capture_provenance(ptr %p) => ptr 0x14 [p]
+; CHECK-NEXT:   store i32 1, ptr %p1, align 4
+; CHECK-NEXT:   %p2 = load ptr, ptr @g, align 8 => ptr 0x14 [p none]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i32 1, ptr %p2, align 4 at @main <stdin>:18
+; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/store_capture_provenance.ll b/llvm/test/tools/llubi/store_capture_provenance.ll
index 920742c9fe5ff..f217ab4137718 100644
--- a/llvm/test/tools/llubi/store_capture_provenance.ll
+++ b/llvm/test/tools/llubi/store_capture_provenance.ll
@@ -9,6 +9,7 @@ define ptr @address_read_provenance(ptr captures(address, read_provenance) %p) {
 
 define void @main() {
   %g_address_read_provenance = call ptr @address_read_provenance(ptr @g)
+  ; Only read_provenance is captured. It is UB to store via the pointer.
   store i8 1, ptr %g_address_read_provenance
   ret void
 }
@@ -19,6 +20,6 @@ define void @main() {
 ; CHECK-NEXT: Exiting function: address_read_provenance
 ; CHECK-NEXT:   %g_address_read_provenance = call ptr @address_read_provenance(ptr @g) => ptr 0x8 [@g read_provenance]
 ; CHECK-NEXT: Stacktrace:
-; CHECK-NEXT: #0   store i8 1, ptr %g_address_read_provenance, align 1 at @main <stdin>:12
+; CHECK-NEXT: #0   store i8 1, ptr %g_address_read_provenance, align 1 at @main <stdin>:13
 ; CHECK-NEXT: Immediate UB detected: Invalid memory access via a pointer with nullary provenance.
 ; CHECK-NEXT: error: Execution of function 'main' failed.



More information about the llvm-commits mailing list