[llvm] [llubi] Add support for byval pointer arguments (PR #201852)

Yingwei Zheng via llvm-commits llvm-commits at lists.llvm.org
Mon Jun 8 09:53:19 PDT 2026


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

>From e31964dd1f984d66a0cc9ec7b9d6bd91e3a8f37b Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Fri, 5 Jun 2026 22:37:50 +0800
Subject: [PATCH 1/4] [llubi] Add support for byval pointer arguments

---
 llvm/test/tools/llubi/byval.ll            | 56 +++++++++++++++++++++++
 llvm/test/tools/llubi/byval_large_size.ll | 18 ++++++++
 llvm/test/tools/llubi/byval_lifetime.ll   | 25 ++++++++++
 llvm/test/tools/llubi/byval_misalign.ll   | 18 ++++++++
 llvm/test/tools/llubi/byval_oversize.ll   | 18 ++++++++
 llvm/test/tools/llubi/byval_padding.ll    | 41 +++++++++++++++++
 llvm/test/tools/llubi/byval_poison.ll     | 16 +++++++
 llvm/tools/llubi/lib/ExecutorBase.h       |  3 ++
 llvm/tools/llubi/lib/Interpreter.cpp      | 46 ++++++++++++++++++-
 9 files changed, 240 insertions(+), 1 deletion(-)
 create mode 100644 llvm/test/tools/llubi/byval.ll
 create mode 100644 llvm/test/tools/llubi/byval_large_size.ll
 create mode 100644 llvm/test/tools/llubi/byval_lifetime.ll
 create mode 100644 llvm/test/tools/llubi/byval_misalign.ll
 create mode 100644 llvm/test/tools/llubi/byval_oversize.ll
 create mode 100644 llvm/test/tools/llubi/byval_padding.ll
 create mode 100644 llvm/test/tools/llubi/byval_poison.ll

diff --git a/llvm/test/tools/llubi/byval.ll b/llvm/test/tools/llubi/byval.ll
new file mode 100644
index 0000000000000..5bbb905b46f0c
--- /dev/null
+++ b/llvm/test/tools/llubi/byval.ll
@@ -0,0 +1,56 @@
+; 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 byval(i32) %p) {
+  store i32 1, ptr %p
+  ret void
+}
+
+define void @write_callsite_byval(ptr %p) {
+  store i32 2, ptr %p
+  ret void
+}
+
+define void @write_underalign(ptr byval(i32) align 1 %p) {
+  store i32 3, ptr %p, align 1
+  ret void
+}
+
+define void @main() {
+  %var = alloca i32
+  store i32 0, ptr %var
+  call void @write(ptr %var)
+  %v1 = load i32, ptr %var
+  call void @write_callsite_byval(ptr byval(i32) %var)
+  %v2 = load i32, ptr %var
+  call void @write_underalign(ptr %var)
+  %v3 = load i32, ptr %var
+
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %var = alloca i32, align 4 => ptr 0x8 [var]
+; CHECK-NEXT:   store i32 0, ptr %var, align 4
+; CHECK-NEXT: Entering function: write
+; CHECK-NEXT:   ptr %p = ptr 0xC [p]
+; CHECK-NEXT:   store i32 1, ptr %p, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: write
+; CHECK-NEXT:   call void @write(ptr %var)
+; CHECK-NEXT:   %v1 = load i32, ptr %var, align 4 => i32 0
+; CHECK-NEXT: Entering function: write_callsite_byval
+; CHECK-NEXT:   ptr %p = ptr 0x10 [p]
+; CHECK-NEXT:   store i32 2, ptr %p, align 4
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: write_callsite_byval
+; CHECK-NEXT:   call void @write_callsite_byval(ptr byval(i32) %var)
+; CHECK-NEXT:   %v2 = load i32, ptr %var, align 4 => i32 0
+; CHECK-NEXT: Entering function: write_underalign
+; CHECK-NEXT:   ptr %p = ptr 0x14 [p]
+; CHECK-NEXT:   store i32 3, ptr %p, align 1
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: write_underalign
+; CHECK-NEXT:   call void @write_underalign(ptr %var)
+; CHECK-NEXT:   %v3 = load i32, ptr %var, align 4 => i32 0
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/byval_large_size.ll b/llvm/test/tools/llubi/byval_large_size.ll
new file mode 100644
index 0000000000000..03c9f0c40f31c
--- /dev/null
+++ b/llvm/test/tools/llubi/byval_large_size.ll
@@ -0,0 +1,18 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --verbose --max-mem=100 < %s 2>&1 | FileCheck %s
+
+define void @callee(ptr byval([50 x i8]) %p) {
+  ret void
+}
+
+define void @main() {
+  %alloc = alloca [50 x i8]
+  call void @callee(ptr %alloc)
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %alloc = alloca [50 x i8], align 1 => ptr 0x8 [alloc]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @callee(ptr %alloc) at @main <stdin>:10
+; CHECK-NEXT: Error: Insufficient stack space for byval pointer argument.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/byval_lifetime.ll b/llvm/test/tools/llubi/byval_lifetime.ll
new file mode 100644
index 0000000000000..578a1c54bdb8a
--- /dev/null
+++ b/llvm/test/tools/llubi/byval_lifetime.ll
@@ -0,0 +1,25 @@
+; 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 @identity(ptr byval(i32) %p) {
+  ret ptr %p
+}
+
+define void @main() {
+  %var = alloca i32
+  %p = call ptr @identity(ptr %var)
+  store i32 0, ptr %p
+
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %var = alloca i32, align 4 => ptr 0x8 [var]
+; CHECK-NEXT: Entering function: identity
+; CHECK-NEXT:   ptr %p = ptr 0xC [p]
+; CHECK-NEXT:   ret ptr %p
+; CHECK-NEXT: Exiting function: identity
+; CHECK-NEXT:   %p = call ptr @identity(ptr %var) => ptr 0xC [p]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   store i32 0, ptr %p, align 4 at @main <stdin>:11
+; CHECK-NEXT: Immediate UB detected: Try to access a dead memory object at address 0xc.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/byval_misalign.ll b/llvm/test/tools/llubi/byval_misalign.ll
new file mode 100644
index 0000000000000..cfd3027f14ce0
--- /dev/null
+++ b/llvm/test/tools/llubi/byval_misalign.ll
@@ -0,0 +1,18 @@
+; 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 void @callee(ptr byval(i32) align 16 %p) {
+  ret void
+}
+
+define void @main() {
+  %alloc = alloca i32
+  call void @callee(ptr %alloc)
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x8 [alloc]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @callee(ptr %alloc) at @main <stdin>:10
+; CHECK-NEXT: Immediate UB detected: Misaligned memory access. Address: 0x8, Required alignment: 16.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/byval_oversize.ll b/llvm/test/tools/llubi/byval_oversize.ll
new file mode 100644
index 0000000000000..55bb3437552d8
--- /dev/null
+++ b/llvm/test/tools/llubi/byval_oversize.ll
@@ -0,0 +1,18 @@
+; 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 void @callee(ptr byval(i32) %p) {
+  ret void
+}
+
+define void @main() {
+  %alloc = alloca i8
+  call void @callee(ptr %alloc)
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %alloc = alloca i8, align 1 => ptr 0x8 [alloc]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @callee(ptr %alloc) at @main <stdin>:10
+; CHECK-NEXT: Immediate UB detected: Memory access is out of bounds. Accessed size: 4, Address: 0x8, Object base: 0x8, Object size: 1.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/byval_padding.ll b/llvm/test/tools/llubi/byval_padding.ll
new file mode 100644
index 0000000000000..042f821097f9d
--- /dev/null
+++ b/llvm/test/tools/llubi/byval_padding.ll
@@ -0,0 +1,41 @@
+; 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-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64"
+
+%struct = type {i8, i32, i8}
+
+define void @callee(ptr byval(%struct) %p) {
+  %concrete_zero1 = load i8, ptr %p
+  %concrete_zero2 = load i8, ptr %p
+  %p_padding1 = getelementptr i8, ptr %p, i64 1
+  %undef1_padding1 = load i8, ptr %p_padding1
+  %undef2_padding1 = load i8, ptr %p_padding1
+  %p_padding2 = getelementptr i8, ptr %p, i64 9
+  %undef1_padding2 = load i8, ptr %p_padding2
+  %undef2_padding2 = load i8, ptr %p_padding2
+  ret void
+}
+
+ at g = global [12 x i8] zeroinitializer
+
+define void @main() {
+  call void @callee(ptr @g)
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT: Entering function: callee
+; CHECK-NEXT:   ptr %p = ptr 0x14 [p]
+; CHECK-NEXT:   %concrete_zero1 = load i8, ptr %p, align 1 => i8 0
+; CHECK-NEXT:   %concrete_zero2 = load i8, ptr %p, align 1 => i8 0
+; CHECK-NEXT:   %p_padding1 = getelementptr i8, ptr %p, i64 1 => ptr 0x15 [p + 1]
+; CHECK-NEXT:   %undef1_padding1 = load i8, ptr %p_padding1, align 1 => i8 62
+; CHECK-NEXT:   %undef2_padding1 = load i8, ptr %p_padding1, align 1 => i8 -117
+; CHECK-NEXT:   %p_padding2 = getelementptr i8, ptr %p, i64 9 => ptr 0x1D [p + 9]
+; CHECK-NEXT:   %undef1_padding2 = load i8, ptr %p_padding2, align 1 => i8 -71
+; CHECK-NEXT:   %undef2_padding2 = load i8, ptr %p_padding2, align 1 => i8 -18
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: callee
+; CHECK-NEXT:   call void @callee(ptr @g)
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/byval_poison.ll b/llvm/test/tools/llubi/byval_poison.ll
new file mode 100644
index 0000000000000..be4e6aae56694
--- /dev/null
+++ b/llvm/test/tools/llubi/byval_poison.ll
@@ -0,0 +1,16 @@
+; 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 void @callee(ptr byval(i32) %p) {
+  ret void
+}
+
+define void @main() {
+  call void @callee(ptr poison)
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @callee(ptr poison) at @main <stdin>:9
+; CHECK-NEXT: Immediate UB detected: Invalid poison byval pointer argument.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/tools/llubi/lib/ExecutorBase.h b/llvm/tools/llubi/lib/ExecutorBase.h
index d07b4dae1792e..d1744e48cab44 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.h
+++ b/llvm/tools/llubi/lib/ExecutorBase.h
@@ -64,6 +64,9 @@ struct Frame {
   // Reserved for in-flight subroutines.
   Function *ResolvedCallee = nullptr;
   SmallVector<AnyValue> CalleeArgs;
+  // Temporary memory objects created via pointer arguments with byval.
+  // They are belong to the caller.
+  SmallVector<IntrusiveRefCntPtr<MemoryObject>> CalleeByValArgs;
   AnyValue CalleeRetVal;
 
   Frame(Function &F, CallBase *CallSite, Frame *LastFrame,
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 81cc968535a2f..6d6a0f57d4756 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -907,6 +907,10 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     }
     setResult(CB, std::move(RetVal));
 
+    for (auto &ByValArgs : CurrentFrame->CalleeByValArgs)
+      Ctx.free(*ByValArgs);
+    CurrentFrame->CalleeByValArgs.clear();
+
     if (auto *II = dyn_cast<InvokeInst>(&CB))
       jumpTo(*II, II->getNormalDest());
     else if (CurrentFrame->State == FrameState::Pending)
@@ -1775,7 +1779,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
 
   void enterCall(CallBase &CB) {
     Function *Callee = CB.getCalledFunction();
-    // TODO: handle byval/initializes
+    // TODO: handle initializes
     auto &CalleeArgs = CurrentFrame->CalleeArgs;
     assert(CalleeArgs.empty() &&
            "Forgot to call returnFromCallee before entering a new call.");
@@ -1826,10 +1830,50 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     for (auto [I, Arg] : enumerate(CB.args())) {
       Type *ArgTy = Arg->getType();
       AnyValue &ArgVal = CalleeArgs[I];
+
       // CallBase::paramHasAttr also checks parameter attributes at known
       // callee. We do it explicitly to avoid duplication.
       AttributeSet AttrsAtCallSite = CB.getParamAttributes(I);
       AttributeSet AttrsAtCallee = Callee->getAttributes().getParamAttrs(I);
+
+      if (auto *ByValTy = CB.getParamByValType(I)) {
+        if (ArgVal.isPoison()) {
+          reportImmediateUB() << "Invalid poison byval pointer argument.";
+          return;
+        }
+
+        uint64_t Size = Ctx.getEffectiveTypeAllocSize(ByValTy);
+        MaybeAlign AllocAlign = AttrsAtCallSite.getAlignment();
+        if (AllocAlign.has_value()) {
+          if (MaybeAlign CalleeAlign = AttrsAtCallee.getAlignment())
+            AllocAlign = std::max(AllocAlign.value(), CalleeAlign.value());
+        } else {
+          AllocAlign = AttrsAtCallee.getAlignment();
+        }
+        if (!AllocAlign.has_value())
+          AllocAlign = DL.getABITypeAlign(ByValTy);
+        // Byval pointers cannot be passed via variadic arguments.
+        auto Obj = Ctx.allocate(
+            Size, AllocAlign.value().value(), Callee->getArg(I)->getName(),
+            ArgTy->getPointerAddressSpace(), MemInitKind::Uninitialized,
+            MemAllocKind::Stack);
+        if (!Obj) {
+          reportError()
+              << "Insufficient stack space for byval pointer argument.";
+          return;
+        }
+        CurrentFrame->CalleeByValArgs.push_back(Obj);
+
+        auto &SrcPtr = ArgVal.asPointer();
+        auto Val = load(SrcPtr, AllocAlign.value(), ByValTy, /*NoUndef=*/false);
+        if (hasProgramExited())
+          return;
+        auto TgtPtr = Ctx.deriveFromMemoryObject(std::move(Obj));
+        store(TgtPtr, AllocAlign.value(), Val, ByValTy);
+        if (hasProgramExited())
+          return;
+        ArgVal = std::move(TgtPtr);
+      }
       handleAttributes(ArgTy, ArgVal, AttrsAtCallSite, AttrsAtCallee);
     }
 

>From f66eab6a3de7edcd16078e9837ba28117bde28ba Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Mon, 8 Jun 2026 08:39:53 +0800
Subject: [PATCH 2/4] [llubi] Address review comments.

---
 llvm/test/tools/llubi/byval.ll            | 18 +++---
 llvm/test/tools/llubi/byval_large_size.ll |  4 +-
 llvm/test/tools/llubi/byval_lifetime.ll   |  4 +-
 llvm/test/tools/llubi/byval_misalign.ll   |  4 +-
 llvm/test/tools/llubi/byval_mismatch1.ll  | 18 ++++++
 llvm/test/tools/llubi/byval_mismatch2.ll  | 18 ++++++
 llvm/test/tools/llubi/byval_mismatch3.ll  | 18 ++++++
 llvm/test/tools/llubi/byval_oversize.ll   |  4 +-
 llvm/test/tools/llubi/byval_padding.ll    |  4 +-
 llvm/test/tools/llubi/byval_poison.ll     |  4 +-
 llvm/tools/llubi/lib/ExecutorBase.h       |  2 +-
 llvm/tools/llubi/lib/Interpreter.cpp      | 75 +++++++++++++----------
 12 files changed, 119 insertions(+), 54 deletions(-)
 create mode 100644 llvm/test/tools/llubi/byval_mismatch1.ll
 create mode 100644 llvm/test/tools/llubi/byval_mismatch2.ll
 create mode 100644 llvm/test/tools/llubi/byval_mismatch3.ll

diff --git a/llvm/test/tools/llubi/byval.ll b/llvm/test/tools/llubi/byval.ll
index 5bbb905b46f0c..0f2df14979070 100644
--- a/llvm/test/tools/llubi/byval.ll
+++ b/llvm/test/tools/llubi/byval.ll
@@ -6,7 +6,7 @@ define void @write(ptr byval(i32) %p) {
   ret void
 }
 
-define void @write_callsite_byval(ptr %p) {
+define void @write_callsite_align(ptr byval(i32) %p) {
   store i32 2, ptr %p
   ret void
 }
@@ -19,11 +19,11 @@ define void @write_underalign(ptr byval(i32) align 1 %p) {
 define void @main() {
   %var = alloca i32
   store i32 0, ptr %var
-  call void @write(ptr %var)
+  call void @write(ptr byval(i32) %var)
   %v1 = load i32, ptr %var
-  call void @write_callsite_byval(ptr byval(i32) %var)
+  call void @write_callsite_align(ptr byval(i32) align 8 %var)
   %v2 = load i32, ptr %var
-  call void @write_underalign(ptr %var)
+  call void @write_underalign(ptr byval(i32) %var)
   %v3 = load i32, ptr %var
 
   ret void
@@ -36,21 +36,21 @@ define void @main() {
 ; CHECK-NEXT:   store i32 1, ptr %p, align 4
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: write
-; CHECK-NEXT:   call void @write(ptr %var)
+; CHECK-NEXT:   call void @write(ptr byval(i32) %var)
 ; CHECK-NEXT:   %v1 = load i32, ptr %var, align 4 => i32 0
-; CHECK-NEXT: Entering function: write_callsite_byval
+; CHECK-NEXT: Entering function: write_callsite_align
 ; CHECK-NEXT:   ptr %p = ptr 0x10 [p]
 ; CHECK-NEXT:   store i32 2, ptr %p, align 4
 ; CHECK-NEXT:   ret void
-; CHECK-NEXT: Exiting function: write_callsite_byval
-; CHECK-NEXT:   call void @write_callsite_byval(ptr byval(i32) %var)
+; CHECK-NEXT: Exiting function: write_callsite_align
+; CHECK-NEXT:   call void @write_callsite_align(ptr byval(i32) align 8 %var)
 ; CHECK-NEXT:   %v2 = load i32, ptr %var, align 4 => i32 0
 ; CHECK-NEXT: Entering function: write_underalign
 ; CHECK-NEXT:   ptr %p = ptr 0x14 [p]
 ; CHECK-NEXT:   store i32 3, ptr %p, align 1
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: write_underalign
-; CHECK-NEXT:   call void @write_underalign(ptr %var)
+; CHECK-NEXT:   call void @write_underalign(ptr byval(i32) %var)
 ; CHECK-NEXT:   %v3 = load i32, ptr %var, align 4 => i32 0
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/byval_large_size.ll b/llvm/test/tools/llubi/byval_large_size.ll
index 03c9f0c40f31c..c252bf13c18ad 100644
--- a/llvm/test/tools/llubi/byval_large_size.ll
+++ b/llvm/test/tools/llubi/byval_large_size.ll
@@ -7,12 +7,12 @@ define void @callee(ptr byval([50 x i8]) %p) {
 
 define void @main() {
   %alloc = alloca [50 x i8]
-  call void @callee(ptr %alloc)
+  call void @callee(ptr byval([50 x i8]) %alloc)
   ret void
 }
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %alloc = alloca [50 x i8], align 1 => ptr 0x8 [alloc]
 ; CHECK-NEXT: Stacktrace:
-; CHECK-NEXT: #0   call void @callee(ptr %alloc) at @main <stdin>:10
+; CHECK-NEXT: #0   call void @callee(ptr byval([50 x i8]) %alloc) at @main <stdin>:10
 ; CHECK-NEXT: Error: Insufficient stack space for byval pointer argument.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/byval_lifetime.ll b/llvm/test/tools/llubi/byval_lifetime.ll
index 578a1c54bdb8a..df5aef9cc497d 100644
--- a/llvm/test/tools/llubi/byval_lifetime.ll
+++ b/llvm/test/tools/llubi/byval_lifetime.ll
@@ -7,7 +7,7 @@ define ptr @identity(ptr byval(i32) %p) {
 
 define void @main() {
   %var = alloca i32
-  %p = call ptr @identity(ptr %var)
+  %p = call ptr @identity(ptr byval(i32) %var)
   store i32 0, ptr %p
 
   ret void
@@ -18,7 +18,7 @@ define void @main() {
 ; CHECK-NEXT:   ptr %p = ptr 0xC [p]
 ; CHECK-NEXT:   ret ptr %p
 ; CHECK-NEXT: Exiting function: identity
-; CHECK-NEXT:   %p = call ptr @identity(ptr %var) => ptr 0xC [p]
+; CHECK-NEXT:   %p = call ptr @identity(ptr byval(i32) %var) => ptr 0xC [p]
 ; CHECK-NEXT: Stacktrace:
 ; CHECK-NEXT: #0   store i32 0, ptr %p, align 4 at @main <stdin>:11
 ; CHECK-NEXT: Immediate UB detected: Try to access a dead memory object at address 0xc.
diff --git a/llvm/test/tools/llubi/byval_misalign.ll b/llvm/test/tools/llubi/byval_misalign.ll
index cfd3027f14ce0..b65ea5dd8fc31 100644
--- a/llvm/test/tools/llubi/byval_misalign.ll
+++ b/llvm/test/tools/llubi/byval_misalign.ll
@@ -7,12 +7,12 @@ define void @callee(ptr byval(i32) align 16 %p) {
 
 define void @main() {
   %alloc = alloca i32
-  call void @callee(ptr %alloc)
+  call void @callee(ptr byval(i32) %alloc)
   ret void
 }
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x8 [alloc]
 ; CHECK-NEXT: Stacktrace:
-; CHECK-NEXT: #0   call void @callee(ptr %alloc) at @main <stdin>:10
+; CHECK-NEXT: #0   call void @callee(ptr byval(i32) %alloc) at @main <stdin>:10
 ; CHECK-NEXT: Immediate UB detected: Misaligned memory access. Address: 0x8, Required alignment: 16.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/byval_mismatch1.ll b/llvm/test/tools/llubi/byval_mismatch1.ll
new file mode 100644
index 0000000000000..17f0fa1837487
--- /dev/null
+++ b/llvm/test/tools/llubi/byval_mismatch1.ll
@@ -0,0 +1,18 @@
+; 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 void @callee(ptr byval(i32) %p) {
+  ret void
+}
+
+define void @main() {
+  %alloc = alloca i32
+  call void @callee(ptr %alloc)
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x8 [alloc]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @callee(ptr %alloc) at @main <stdin>:10
+; CHECK-NEXT: Immediate UB detected: Mismatched byval attribute between callee and callsite.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/byval_mismatch2.ll b/llvm/test/tools/llubi/byval_mismatch2.ll
new file mode 100644
index 0000000000000..68b025eddf48b
--- /dev/null
+++ b/llvm/test/tools/llubi/byval_mismatch2.ll
@@ -0,0 +1,18 @@
+; 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 void @callee(ptr %p) {
+  ret void
+}
+
+define void @main() {
+  %alloc = alloca i32
+  call void @callee(ptr byval(i32) %alloc)
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x8 [alloc]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @callee(ptr byval(i32) %alloc) at @main <stdin>:10
+; CHECK-NEXT: Immediate UB detected: Mismatched byval attribute between callee and callsite.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/byval_mismatch3.ll b/llvm/test/tools/llubi/byval_mismatch3.ll
new file mode 100644
index 0000000000000..379af24de53f2
--- /dev/null
+++ b/llvm/test/tools/llubi/byval_mismatch3.ll
@@ -0,0 +1,18 @@
+; 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 void @callee(ptr byval([4 x i8]) %p) {
+  ret void
+}
+
+define void @main() {
+  %alloc = alloca i32
+  call void @callee(ptr byval(i32) %alloc)
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %alloc = alloca i32, align 4 => ptr 0x8 [alloc]
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @callee(ptr byval(i32) %alloc) at @main <stdin>:10
+; CHECK-NEXT: Immediate UB detected: Mismatched byval attribute between callee and callsite.
+; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/byval_oversize.ll b/llvm/test/tools/llubi/byval_oversize.ll
index 55bb3437552d8..821d69463b0f8 100644
--- a/llvm/test/tools/llubi/byval_oversize.ll
+++ b/llvm/test/tools/llubi/byval_oversize.ll
@@ -7,12 +7,12 @@ define void @callee(ptr byval(i32) %p) {
 
 define void @main() {
   %alloc = alloca i8
-  call void @callee(ptr %alloc)
+  call void @callee(ptr byval(i32) %alloc)
   ret void
 }
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %alloc = alloca i8, align 1 => ptr 0x8 [alloc]
 ; CHECK-NEXT: Stacktrace:
-; CHECK-NEXT: #0   call void @callee(ptr %alloc) at @main <stdin>:10
+; CHECK-NEXT: #0   call void @callee(ptr byval(i32) %alloc) at @main <stdin>:10
 ; CHECK-NEXT: Immediate UB detected: Memory access is out of bounds. Accessed size: 4, Address: 0x8, Object base: 0x8, Object size: 1.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/test/tools/llubi/byval_padding.ll b/llvm/test/tools/llubi/byval_padding.ll
index 042f821097f9d..63814735842c6 100644
--- a/llvm/test/tools/llubi/byval_padding.ll
+++ b/llvm/test/tools/llubi/byval_padding.ll
@@ -20,7 +20,7 @@ define void @callee(ptr byval(%struct) %p) {
 @g = global [12 x i8] zeroinitializer
 
 define void @main() {
-  call void @callee(ptr @g)
+  call void @callee(ptr byval(%struct) @g)
   ret void
 }
 ; CHECK: Entering function: main
@@ -36,6 +36,6 @@ define void @main() {
 ; CHECK-NEXT:   %undef2_padding2 = load i8, ptr %p_padding2, align 1 => i8 -18
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: callee
-; CHECK-NEXT:   call void @callee(ptr @g)
+; CHECK-NEXT:   call void @callee(ptr byval(%struct) @g)
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/byval_poison.ll b/llvm/test/tools/llubi/byval_poison.ll
index be4e6aae56694..fdc8641ed886a 100644
--- a/llvm/test/tools/llubi/byval_poison.ll
+++ b/llvm/test/tools/llubi/byval_poison.ll
@@ -6,11 +6,11 @@ define void @callee(ptr byval(i32) %p) {
 }
 
 define void @main() {
-  call void @callee(ptr poison)
+  call void @callee(ptr byval(i32) poison)
   ret void
 }
 ; CHECK: Entering function: main
 ; CHECK-NEXT: Stacktrace:
-; CHECK-NEXT: #0   call void @callee(ptr poison) at @main <stdin>:9
+; CHECK-NEXT: #0   call void @callee(ptr byval(i32) poison) at @main <stdin>:9
 ; CHECK-NEXT: Immediate UB detected: Invalid poison byval pointer argument.
 ; CHECK-NEXT: error: Execution of function 'main' failed.
diff --git a/llvm/tools/llubi/lib/ExecutorBase.h b/llvm/tools/llubi/lib/ExecutorBase.h
index d1744e48cab44..424e1a1774202 100644
--- a/llvm/tools/llubi/lib/ExecutorBase.h
+++ b/llvm/tools/llubi/lib/ExecutorBase.h
@@ -65,7 +65,7 @@ struct Frame {
   Function *ResolvedCallee = nullptr;
   SmallVector<AnyValue> CalleeArgs;
   // Temporary memory objects created via pointer arguments with byval.
-  // They are belong to the caller.
+  // They belong to the caller.
   SmallVector<IntrusiveRefCntPtr<MemoryObject>> CalleeByValArgs;
   AnyValue CalleeRetVal;
 
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 6d6a0f57d4756..4bb0f86ed8bc9 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -1836,43 +1836,54 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
       AttributeSet AttrsAtCallSite = CB.getParamAttributes(I);
       AttributeSet AttrsAtCallee = Callee->getAttributes().getParamAttrs(I);
 
-      if (auto *ByValTy = CB.getParamByValType(I)) {
-        if (ArgVal.isPoison()) {
-          reportImmediateUB() << "Invalid poison byval pointer argument.";
+      if (ArgTy->isPointerTy()) {
+        auto *ByValTy = AttrsAtCallSite.getByValType();
+        auto *ByValTyFromCallee = AttrsAtCallee.getByValType();
+        if (ByValTy != ByValTyFromCallee) {
+          reportImmediateUB()
+              << "Mismatched byval attribute between callee and callsite.";
           return;
         }
+        if (ByValTy) {
+          if (ArgVal.isPoison()) {
+            reportImmediateUB() << "Invalid poison byval pointer argument.";
+            return;
+          }
 
-        uint64_t Size = Ctx.getEffectiveTypeAllocSize(ByValTy);
-        MaybeAlign AllocAlign = AttrsAtCallSite.getAlignment();
-        if (AllocAlign.has_value()) {
+          uint64_t Size = Ctx.getEffectiveTypeAllocSize(ByValTy);
+          MaybeAlign AllocAlign = AttrsAtCallSite.getAlignment();
+          // Ignore the alignment at the callsite when it is set on the callee.
           if (MaybeAlign CalleeAlign = AttrsAtCallee.getAlignment())
-            AllocAlign = std::max(AllocAlign.value(), CalleeAlign.value());
-        } else {
-          AllocAlign = AttrsAtCallee.getAlignment();
-        }
-        if (!AllocAlign.has_value())
-          AllocAlign = DL.getABITypeAlign(ByValTy);
-        // Byval pointers cannot be passed via variadic arguments.
-        auto Obj = Ctx.allocate(
-            Size, AllocAlign.value().value(), Callee->getArg(I)->getName(),
-            ArgTy->getPointerAddressSpace(), MemInitKind::Uninitialized,
-            MemAllocKind::Stack);
-        if (!Obj) {
-          reportError()
-              << "Insufficient stack space for byval pointer argument.";
-          return;
-        }
-        CurrentFrame->CalleeByValArgs.push_back(Obj);
+            AllocAlign = CalleeAlign;
+          if (!AllocAlign.has_value()) {
+            // If the alignment is not specified, we use the default ABI
+            // alignment. This is the default behavior of
+            // TargetLoweringBase::getByValTypeAlignment.
+            AllocAlign = DL.getABITypeAlign(ByValTy);
+          }
+          // Byval pointers cannot be passed via variadic arguments.
+          auto Obj = Ctx.allocate(
+              Size, AllocAlign.value().value(), Callee->getArg(I)->getName(),
+              ArgTy->getPointerAddressSpace(), MemInitKind::Uninitialized,
+              MemAllocKind::Stack);
+          if (!Obj) {
+            reportError()
+                << "Insufficient stack space for byval pointer argument.";
+            return;
+          }
+          CurrentFrame->CalleeByValArgs.push_back(Obj);
 
-        auto &SrcPtr = ArgVal.asPointer();
-        auto Val = load(SrcPtr, AllocAlign.value(), ByValTy, /*NoUndef=*/false);
-        if (hasProgramExited())
-          return;
-        auto TgtPtr = Ctx.deriveFromMemoryObject(std::move(Obj));
-        store(TgtPtr, AllocAlign.value(), Val, ByValTy);
-        if (hasProgramExited())
-          return;
-        ArgVal = std::move(TgtPtr);
+          auto &SrcPtr = ArgVal.asPointer();
+          auto Val =
+              load(SrcPtr, AllocAlign.value(), ByValTy, /*NoUndef=*/false);
+          if (hasProgramExited())
+            return;
+          auto TgtPtr = Ctx.deriveFromMemoryObject(std::move(Obj));
+          store(TgtPtr, AllocAlign.value(), Val, ByValTy);
+          if (hasProgramExited())
+            return;
+          ArgVal = std::move(TgtPtr);
+        }
       }
       handleAttributes(ArgTy, ArgVal, AttrsAtCallSite, AttrsAtCallee);
     }

>From e9702fe66de21b6213060533f75113460ab23e4e Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Tue, 9 Jun 2026 00:50:18 +0800
Subject: [PATCH 3/4] [llubi] Address review comments. NFC.

---
 llvm/test/tools/llubi/byval_null.ll  | 16 ++++++++++++++++
 llvm/tools/llubi/lib/Interpreter.cpp |  6 +++---
 2 files changed, 19 insertions(+), 3 deletions(-)
 create mode 100644 llvm/test/tools/llubi/byval_null.ll

diff --git a/llvm/test/tools/llubi/byval_null.ll b/llvm/test/tools/llubi/byval_null.ll
new file mode 100644
index 0000000000000..7e4f1a423f04e
--- /dev/null
+++ b/llvm/test/tools/llubi/byval_null.ll
@@ -0,0 +1,16 @@
+; 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 void @callee(ptr byval(i32) %p) {
+  ret void
+}
+
+define void @main() {
+  call void @callee(ptr byval(i32) null)
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT: Stacktrace:
+; CHECK-NEXT: #0   call void @callee(ptr byval(i32) null) at @main <stdin>:9
+; 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/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 4bb0f86ed8bc9..1ae6dc3de713b 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -907,8 +907,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     }
     setResult(CB, std::move(RetVal));
 
-    for (auto &ByValArgs : CurrentFrame->CalleeByValArgs)
-      Ctx.free(*ByValArgs);
+    for (auto &ByValArg : CurrentFrame->CalleeByValArgs)
+      Ctx.free(*ByValArg);
     CurrentFrame->CalleeByValArgs.clear();
 
     if (auto *II = dyn_cast<InvokeInst>(&CB))
@@ -1863,7 +1863,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
           }
           // Byval pointers cannot be passed via variadic arguments.
           auto Obj = Ctx.allocate(
-              Size, AllocAlign.value().value(), Callee->getArg(I)->getName(),
+              Size, AllocAlign->value(), Callee->getArg(I)->getName(),
               ArgTy->getPointerAddressSpace(), MemInitKind::Uninitialized,
               MemAllocKind::Stack);
           if (!Obj) {

>From 2b808fae13a0ac9be6a1ca50867a944a87ca3fd3 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Tue, 9 Jun 2026 00:52:57 +0800
Subject: [PATCH 4/4] [llubi] Convert comments into assertions.

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

diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 1ae6dc3de713b..6242c9cef288f 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -1861,7 +1861,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
             // TargetLoweringBase::getByValTypeAlignment.
             AllocAlign = DL.getABITypeAlign(ByValTy);
           }
-          // Byval pointers cannot be passed via variadic arguments.
+          assert(I < Callee->arg_size() &&
+                 "Byval pointers cannot be passed via variadic arguments.");
           auto Obj = Ctx.allocate(
               Size, AllocAlign->value(), Callee->getArg(I)->getName(),
               ArgTy->getPointerAddressSpace(), MemInitKind::Uninitialized,



More information about the llvm-commits mailing list