[llvm] [CopyProf] Add CopyProf instrumentation passes. (PR #207385)

via llvm-commits llvm-commits at lists.llvm.org
Wed Jul 29 00:47:45 PDT 2026


https://github.com/newgre updated https://github.com/llvm/llvm-project/pull/207385

>From 8d1201d17174529b4848845d5e0f356e7475ab07 Mon Sep 17 00:00:00 2001
From: Jan Newger <jannewger at gmail.com>
Date: Tue, 30 Jun 2026 10:53:09 +0000
Subject: [PATCH 1/5] [CopyProf] Add CopyProf instrumentation passes.

This patch introduces the instrumentation passes and corresponding tests
for CopyProf, a profiling tool designed to identify unnecessary object
copies in C++ applications.
RFC at
https://discourse.llvm.org/t/rfc-copysanitizer-csan-detecting-unneccessary-object-copies-at-runtime/91038.

Three passes are added:
- CopyProfPass inserts enter/exit callback around special member
functions.
- CopyPRofStoresPass instruments store instructions  to track memory
modifications.
- ModuleCopyProfPass inserts a module constructor to initialize the
CopyProf runtime at program startup (will be added later).
---
 .../Transforms/Instrumentation/CopyProf.h     |  52 +++
 llvm/lib/Passes/PassBuilder.cpp               |   1 +
 llvm/lib/Passes/PassRegistry.def              |   3 +
 .../Transforms/Instrumentation/CMakeLists.txt |   1 +
 .../Transforms/Instrumentation/CopyProf.cpp   | 297 ++++++++++++++++++
 .../CopyProf/function-instrumentation.ll      | 135 ++++++++
 .../CopyProf/no-instrumentation.ll            |  66 ++++
 .../CopyProf/store-instrumentation.ll         | 101 ++++++
 8 files changed, 656 insertions(+)
 create mode 100644 llvm/include/llvm/Transforms/Instrumentation/CopyProf.h
 create mode 100644 llvm/lib/Transforms/Instrumentation/CopyProf.cpp
 create mode 100644 llvm/test/Instrumentation/CopyProf/function-instrumentation.ll
 create mode 100644 llvm/test/Instrumentation/CopyProf/no-instrumentation.ll
 create mode 100644 llvm/test/Instrumentation/CopyProf/store-instrumentation.ll

diff --git a/llvm/include/llvm/Transforms/Instrumentation/CopyProf.h b/llvm/include/llvm/Transforms/Instrumentation/CopyProf.h
new file mode 100644
index 0000000000000..73c49024c52f5
--- /dev/null
+++ b/llvm/include/llvm/Transforms/Instrumentation/CopyProf.h
@@ -0,0 +1,52 @@
+//===-- CopyProf.h ----------------------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file declares the instrumentation passes for CopyProf that insert
+// callbacks into special member functions, and add store instrumentation.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_TRANSFORMS_INSTRUMENTATION_COPYPROF_H
+#define LLVM_TRANSFORMS_INSTRUMENTATION_COPYPROF_H
+
+#include "llvm/IR/PassManager.h"
+namespace llvm {
+
+// Early-stage pass that instruments special member functions to call into the
+// CopyProf runtime.
+class CopyProfPass : public PassInfoMixin<CopyProfPass> {
+public:
+  CopyProfPass() = default;
+  PreservedAnalyses run(Function &F, FunctionAnalysisManager &FAM);
+
+  static bool isRequired() { return true; }
+};
+
+// Module-level pass that inserts the CopyProf runtime initialization
+// constructor and hooks it into @llvm.global_ctors.
+class ModuleCopyProfPass : public PassInfoMixin<ModuleCopyProfPass> {
+public:
+  ModuleCopyProfPass() = default;
+  PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);
+
+  static bool isRequired() { return true; }
+};
+
+// Late-stage pass that instruments store instructions to detect whether an
+// object copy has been modified before it is destructed.
+class CopyProfStoresPass : public PassInfoMixin<CopyProfStoresPass> {
+public:
+  CopyProfStoresPass() = default;
+  PreservedAnalyses run(Function &F, FunctionAnalysisManager &FAM);
+
+  static bool isRequired() { return true; }
+};
+
+} // namespace llvm
+
+#endif // LLVM_TRANSFORMS_INSTRUMENTATION_COPYPROF_H
diff --git a/llvm/lib/Passes/PassBuilder.cpp b/llvm/lib/Passes/PassBuilder.cpp
index 4bc9e0c74c539..b8b5507447fd4 100644
--- a/llvm/lib/Passes/PassBuilder.cpp
+++ b/llvm/lib/Passes/PassBuilder.cpp
@@ -262,6 +262,7 @@
 #include "llvm/Transforms/Instrumentation/BoundsChecking.h"
 #include "llvm/Transforms/Instrumentation/CGProfile.h"
 #include "llvm/Transforms/Instrumentation/ControlHeightReduction.h"
+#include "llvm/Transforms/Instrumentation/CopyProf.h"
 #include "llvm/Transforms/Instrumentation/DataFlowSanitizer.h"
 #include "llvm/Transforms/Instrumentation/GCOVProfiler.h"
 #include "llvm/Transforms/Instrumentation/HWAddressSanitizer.h"
diff --git a/llvm/lib/Passes/PassRegistry.def b/llvm/lib/Passes/PassRegistry.def
index bb5814f377f6b..cba01aab49963 100644
--- a/llvm/lib/Passes/PassRegistry.def
+++ b/llvm/lib/Passes/PassRegistry.def
@@ -63,6 +63,7 @@ MODULE_PASS("called-value-propagation", CalledValuePropagationPass())
 MODULE_PASS("canonicalize-aliases", CanonicalizeAliasesPass())
 MODULE_PASS("check-debugify", NewPMCheckDebugifyPass())
 MODULE_PASS("constmerge", ConstantMergePass())
+MODULE_PASS("copyprof-module", ModuleCopyProfPass())
 MODULE_PASS("coro-cleanup", CoroCleanupPass())
 MODULE_PASS("coro-early", CoroEarlyPass())
 MODULE_PASS("lower-comment-string", LowerCommentStringPass())
@@ -433,6 +434,8 @@ FUNCTION_PASS("codegenprepare", CodeGenPreparePass(*TM))
 FUNCTION_PASS("complex-deinterleaving", ComplexDeinterleavingPass(*TM))
 FUNCTION_PASS("consthoist", ConstantHoistingPass())
 FUNCTION_PASS("constraint-elimination", ConstraintEliminationPass())
+FUNCTION_PASS("copyprof", CopyProfPass())
+FUNCTION_PASS("copyprof-stores", CopyProfStoresPass())
 FUNCTION_PASS("coro-elide", CoroElidePass())
 FUNCTION_PASS("correlated-propagation", CorrelatedValuePropagationPass())
 FUNCTION_PASS("count-visits", CountVisitsPass())
diff --git a/llvm/lib/Transforms/Instrumentation/CMakeLists.txt b/llvm/lib/Transforms/Instrumentation/CMakeLists.txt
index 80576c61fd80c..7d2e658acb5c1 100644
--- a/llvm/lib/Transforms/Instrumentation/CMakeLists.txt
+++ b/llvm/lib/Transforms/Instrumentation/CMakeLists.txt
@@ -4,6 +4,7 @@ add_llvm_component_library(LLVMInstrumentation
   BoundsChecking.cpp
   CGProfile.cpp
   ControlHeightReduction.cpp
+  CopyProf.cpp
   DataFlowSanitizer.cpp
   GCOVProfiling.cpp
   BlockCoverageInference.cpp
diff --git a/llvm/lib/Transforms/Instrumentation/CopyProf.cpp b/llvm/lib/Transforms/Instrumentation/CopyProf.cpp
new file mode 100644
index 0000000000000..6efc318b20195
--- /dev/null
+++ b/llvm/lib/Transforms/Instrumentation/CopyProf.cpp
@@ -0,0 +1,297 @@
+//===-- CopyProf.cpp ------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+///
+/// This file implements the LLVM IR instrumentation passes for CopyProf.
+/// It adds enter/exit callbacks to C++ special member functions, and
+/// instruments store instructions.
+///
+/// The basic idea of the CopyProf algorithm works like this:
+/// An object copy Y is made from original object X. The shadow memory
+/// corresponding to (and owned by) Y is marked as "copied". Any subsequent
+/// memory store to the memory corresponding to Y marks the shadow memory as
+/// "modified". When Y is destroyed and all of its corresponding shadow memory
+/// is marked as "copied", the object is reported as an unnecessary copy.
+///
+//===----------------------------------------------------------------------===//
+
+#include "llvm/Transforms/Instrumentation/CopyProf.h"
+
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringExtras.h"
+#include "llvm/IR/Attributes.h"
+#include "llvm/IR/DerivedTypes.h"
+#include "llvm/IR/Function.h"
+#include "llvm/IR/IRBuilder.h"
+#include "llvm/IR/Instruction.h"
+#include "llvm/IR/Instructions.h"
+#include "llvm/IR/Module.h"
+#include "llvm/IR/PassManager.h"
+#include "llvm/Support/Casting.h"
+#include "llvm/Support/ErrorHandling.h"
+#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Transforms/Utils/Instrumentation.h"
+#include "llvm/Transforms/Utils/ModuleUtils.h"
+#include <array>
+#include <cstddef>
+#include <cstdint>
+
+// TODO: Convert CopyProfPass and CopyProfStoresPass to module passes so that
+// the runtime callbacks can be cached, thus avoiding repetitive symbol table
+// lookups.
+
+using namespace llvm;
+
+// Names for the module c'tor to initialize the runtime, and the runtime
+// initialization function itself.
+constexpr StringRef CopyProfModuleCtorName = "copyprof.module_ctor";
+constexpr StringRef CopyProfInitName = "__copyprof_init";
+
+// Runtime callback function names.
+constexpr StringRef CopyProfCtorEnterCallbackName =
+    "__copyprof_ctor_enter_callback";
+constexpr StringRef CopyProfCtorExitCallbackName =
+    "__copyprof_ctor_exit_callback";
+constexpr StringRef CopyProfCopyCtorEnterCallbackName =
+    "__copyprof_copy_ctor_enter_callback";
+constexpr StringRef CopyProfCopyCtorExitCallbackName =
+    "__copyprof_copy_ctor_exit_callback";
+constexpr StringRef CopyProfCopyAssignOpEnterCallbackName =
+    "__copyprof_copy_assign_op_enter_callback";
+constexpr StringRef CopyProfCopyAssignOpExitCallbackName =
+    "__copyprof_copy_assign_op_exit_callback";
+constexpr StringRef CopyProfDtorEnterCallbackName =
+    "__copyprof_dtor_enter_callback";
+constexpr StringRef CopyProfDtorExitCallbackName =
+    "__copyprof_dtor_exit_callback";
+constexpr StringRef CopyProfStoreCallbackName = "__copyprof_store_callback";
+
+// Attribute strings used by the frontend to mark special member functions.
+constexpr StringRef CopyProfCtorAttr = "copyprof-ctor";
+constexpr StringRef CopyProfCopyCtorAttr = "copyprof-copy-ctor";
+constexpr StringRef CopyProfCopyAssignAttr = "copyprof-copy-assign-op";
+constexpr StringRef CopyProfDtorAttr = "copyprof-dtor";
+
+static bool insertModuleCtor(Module &M) {
+  bool Modified = false;
+  getOrCreateSanitizerCtorAndInitFunctions(
+      M, CopyProfModuleCtorName, CopyProfInitName,
+      /*InitArgTypes=*/{},
+      /*InitArgs=*/{}, [&](Function *Ctor, FunctionCallee) {
+        appendToGlobalCtors(M, Ctor, 0);
+        Modified = true;
+      });
+  return Modified;
+}
+
+// Returns the object size in bytes that was stored in the given function
+// attribute during parsing in the frontend.
+static size_t getAttrValueAsInt(const Function &F, StringRef Attr) {
+  size_t IntValue = 0;
+  if (!to_integer<size_t>(F.getFnAttribute(Attr).getValueAsString(), IntValue,
+                          /*Base=*/10)) {
+    report_fatal_error(formatv("Unable to parse integer value from function "
+                               "attribute value in '{0}': {1}:{2}",
+                               F.getName(), Attr,
+                               F.getFnAttribute(Attr).getValueAsString()));
+  }
+  return IntValue;
+}
+
+namespace {
+
+// Instruments special member functions to call into the CopyProf runtime.
+class CopyProf {
+public:
+  explicit CopyProf(Module &M);
+  bool instrumentFunction(Function &F);
+
+private:
+  void insertCallback(Function &F, size_t ObjSize, unsigned NumArgs,
+                      FunctionCallee Callback, FunctionCallee ExitCallback);
+
+  LLVMContext *Ctx;
+  Type *IntPtrTy;
+  FunctionCallee CtorEnterCallback;
+  FunctionCallee CtorExitCallback;
+  FunctionCallee CopyCtorEnterCallback;
+  FunctionCallee CopyCtorExitCallback;
+  FunctionCallee CopyAssignOpEnterCallback;
+  FunctionCallee CopyAssignOpExitCallback;
+  FunctionCallee DtorEnterCallback;
+  FunctionCallee DtorExitCallback;
+};
+
+// Late-stage pass that instruments store instructions after all optimizations
+// have run (to avoid instrumenting stores that would be eliminated).
+class CopyProfStores {
+public:
+  explicit CopyProfStores(Module &M);
+  bool instrumentFunction(Function &F);
+
+private:
+  Type *IntPtrTy;
+  FunctionCallee StoreCallback;
+};
+
+} // namespace
+
+CopyProf::CopyProf(Module &M) {
+  Ctx = &M.getContext();
+  IRBuilder<> IRB(*Ctx);
+  IntPtrTy = IRB.getIntPtrTy(M.getDataLayout());
+  Type *PtrTy = IRB.getPtrTy();
+  Type *VoidTy = IRB.getVoidTy();
+  // CopyProf callbacks never throw exceptions.
+  AttributeList Attr;
+  Attr = Attr.addFnAttribute(*Ctx, Attribute::NoUnwind);
+  CtorEnterCallback = M.getOrInsertFunction(CopyProfCtorEnterCallbackName, Attr,
+                                            VoidTy, PtrTy, IntPtrTy);
+  CtorExitCallback = M.getOrInsertFunction(CopyProfCtorExitCallbackName, Attr,
+                                           VoidTy, PtrTy, IntPtrTy);
+  CopyCtorEnterCallback = M.getOrInsertFunction(
+      CopyProfCopyCtorEnterCallbackName, Attr, VoidTy, PtrTy, PtrTy, IntPtrTy);
+  CopyCtorExitCallback = M.getOrInsertFunction(
+      CopyProfCopyCtorExitCallbackName, Attr, VoidTy, PtrTy, PtrTy, IntPtrTy);
+  CopyAssignOpEnterCallback =
+      M.getOrInsertFunction(CopyProfCopyAssignOpEnterCallbackName, Attr, VoidTy,
+                            PtrTy, PtrTy, IntPtrTy);
+  CopyAssignOpExitCallback =
+      M.getOrInsertFunction(CopyProfCopyAssignOpExitCallbackName, Attr, VoidTy,
+                            PtrTy, PtrTy, IntPtrTy);
+  DtorEnterCallback = M.getOrInsertFunction(CopyProfDtorEnterCallbackName, Attr,
+                                            VoidTy, PtrTy, IntPtrTy);
+  DtorExitCallback = M.getOrInsertFunction(CopyProfDtorExitCallbackName, Attr,
+                                           VoidTy, PtrTy, IntPtrTy);
+}
+
+bool CopyProf::instrumentFunction(Function &F) {
+  // Must not instrument our own module c'tor or functions that are explicitly
+  // disallowed for instrumentation.
+  if (F.isDeclaration() ||
+      F.hasFnAttribute(Attribute::DisableSanitizerInstrumentation) ||
+      F.getName() == CopyProfModuleCtorName)
+    return false;
+
+  bool Modified = true;
+  if (F.hasFnAttribute(CopyProfCtorAttr))
+    insertCallback(F, getAttrValueAsInt(F, CopyProfCtorAttr), /*NumArgs=*/1,
+                   CtorEnterCallback, CtorExitCallback);
+  else if (F.hasFnAttribute(CopyProfCopyCtorAttr))
+    insertCallback(F, getAttrValueAsInt(F, CopyProfCopyCtorAttr), /*NumArgs=*/2,
+                   CopyCtorEnterCallback, CopyCtorExitCallback);
+  else if (F.hasFnAttribute(CopyProfCopyAssignAttr))
+    insertCallback(F, getAttrValueAsInt(F, CopyProfCopyAssignAttr),
+                   /*NumArgs=*/2, CopyAssignOpEnterCallback,
+                   CopyAssignOpExitCallback);
+  else if (F.hasFnAttribute(CopyProfDtorAttr))
+    insertCallback(F, getAttrValueAsInt(F, CopyProfDtorAttr), /*NumArgs=*/1,
+                   DtorEnterCallback, DtorExitCallback);
+  else
+    Modified = false;
+
+  return Modified;
+}
+
+void CopyProf::insertCallback(Function &F, size_t ObjSize, unsigned NumArgs,
+                              FunctionCallee EntryCallback,
+                              FunctionCallee ExitCallback) {
+  auto InsertCallback = [IntPtrTy = IntPtrTy, ObjSize,
+                         NumArgs](Function &F, InstrumentationIRBuilder &&IRB,
+                                  FunctionCallee Callback) {
+    SmallVector<Value *, 3> Args;
+    // `this` is always the first argument to a special member function, but
+    // copy c'tor / copy assignment operator will have the other `this` ptr
+    // passed as their second argument.
+    assert(NumArgs == 1 || NumArgs == 2);
+    for (unsigned I = 0; I < NumArgs; ++I)
+      Args.push_back(F.getArg(I));
+    // The last argument to the callback is the static size of the object
+    // pointed at by `this`.
+    Args.push_back(ConstantInt::get(IntPtrTy, ObjSize));
+    IRB.CreateCall(Callback, Args);
+  };
+
+  InsertCallback(
+      F,
+      InstrumentationIRBuilder{&F.getEntryBlock(),
+                               F.getEntryBlock().getFirstNonPHIOrDbgOrAlloca()},
+      EntryCallback);
+  for (BasicBlock &BB : F) {
+    Instruction *Term = BB.getTerminator();
+    if (isa<ReturnInst>(Term) || isa<ResumeInst>(Term))
+      InsertCallback(F, InstrumentationIRBuilder{Term}, ExitCallback);
+  }
+}
+
+CopyProfStores::CopyProfStores(Module &M) {
+  LLVMContext &Ctx = M.getContext();
+  IRBuilder<> IRB(Ctx);
+  IntPtrTy = IRB.getIntPtrTy(M.getDataLayout());
+  Type *PtrTy = IRB.getPtrTy();
+  Type *VoidTy = IRB.getVoidTy();
+  // CopyProf callbacks never throw exceptions.
+  AttributeList Attr;
+  Attr = Attr.addFnAttribute(Ctx, Attribute::NoUnwind);
+  StoreCallback = M.getOrInsertFunction(CopyProfStoreCallbackName, Attr, VoidTy,
+                                        PtrTy, IntPtrTy);
+}
+
+bool CopyProfStores::instrumentFunction(Function &F) {
+  if (F.isDeclaration() ||
+      F.hasFnAttribute(Attribute::DisableSanitizerInstrumentation) ||
+      F.getName() == CopyProfModuleCtorName)
+    return false;
+
+  // TODO: handle all types of memory stores (memory intrinsics, masked store
+  // intrinsics, AtomicRMW, and AtomicCmpXchg).
+  SmallVector<StoreInst *, 16> ToInstrument;
+  for (BasicBlock &BB : F) {
+    for (Instruction &I : BB) {
+      if (auto *SI = dyn_cast<StoreInst>(&I);
+          SI != nullptr && SI->getPointerAddressSpace() == 0 &&
+          !SI->hasMetadata(LLVMContext::MD_nosanitize))
+        ToInstrument.push_back(SI);
+    }
+  }
+  if (ToInstrument.empty())
+    return false;
+
+  const DataLayout &DL = F.getParent()->getDataLayout();
+  for (StoreInst *SI : ToInstrument) {
+    uint64_t StoredSize = DL.getTypeStoreSize(SI->getValueOperand()->getType());
+    InstrumentationIRBuilder IRB(SI);
+    std::array<Value *, 2> Args = {SI->getPointerOperand(),
+                                   ConstantInt::get(IntPtrTy, StoredSize)};
+    IRB.CreateCall(StoreCallback, Args);
+  }
+  return true;
+}
+
+PreservedAnalyses CopyProfPass::run(Function &F, FunctionAnalysisManager &) {
+  CopyProf Impl(*F.getParent());
+  if (!Impl.instrumentFunction(F))
+    return PreservedAnalyses::all();
+  PreservedAnalyses PA;
+  PA.preserveSet<CFGAnalyses>();
+  return PA;
+}
+
+PreservedAnalyses ModuleCopyProfPass::run(Module &M, ModuleAnalysisManager &) {
+  return insertModuleCtor(M) ? PreservedAnalyses::none()
+                             : PreservedAnalyses::all();
+}
+
+PreservedAnalyses CopyProfStoresPass::run(Function &F,
+                                          FunctionAnalysisManager &) {
+  CopyProfStores Impl(*F.getParent());
+  if (!Impl.instrumentFunction(F))
+    return PreservedAnalyses::all();
+  PreservedAnalyses PA;
+  PA.preserveSet<CFGAnalyses>();
+  return PA;
+}
diff --git a/llvm/test/Instrumentation/CopyProf/function-instrumentation.ll b/llvm/test/Instrumentation/CopyProf/function-instrumentation.ll
new file mode 100644
index 0000000000000..dd69dd7914250
--- /dev/null
+++ b/llvm/test/Instrumentation/CopyProf/function-instrumentation.ll
@@ -0,0 +1,135 @@
+; Tests basic CopyProf instrumentation of special member functions.
+;
+; RUN: opt < %s -passes='function(copyprof),module(copyprof-module)' -S | FileCheck %s
+
+target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"
+target triple = "x86_64-unknown-linux-gnu"
+
+;; Verifies that the module constructor and global ctors are set up.
+; CHECK: @llvm.global_ctors = appending global [1 x { i32, ptr, ptr }]
+; CHECK-SAME: i32 0, ptr @copyprof.module_ctor
+
+;; Tests constructor instrumentation (1-arg: this).
+define void @ctor(ptr %this) "copyprof-ctor"="24" {
+entry:
+  %field = getelementptr i8, ptr %this, i64 8
+  store i32 0, ptr %field
+  ret void
+}
+; CHECK-LABEL: define void @ctor(ptr %this)
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    call void @__copyprof_ctor_enter_callback(ptr %this, i64 24)
+; CHECK-NOT:     call void @__copyprof_ctor_enter_callback
+; CHECK:         call void @__copyprof_ctor_exit_callback(ptr %this, i64 24)
+; CHECK-NEXT:    ret void
+
+;; Tests copy constructor instrumentation (2-arg: this, other).
+define void @copy_ctor(ptr %this, ptr %other) "copyprof-copy-ctor"="16" {
+entry:
+  %val = load i32, ptr %other
+  store i32 %val, ptr %this
+  ret void
+}
+; CHECK-LABEL: define void @copy_ctor(ptr %this, ptr %other)
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    call void @__copyprof_copy_ctor_enter_callback(ptr %this, ptr %other, i64 16)
+; CHECK-NOT:     call void @__copyprof_copy_ctor_enter_callback
+; CHECK:         call void @__copyprof_copy_ctor_exit_callback(ptr %this, ptr %other, i64 16)
+; CHECK-NEXT:    ret void
+
+;; Tests copy assignment operator instrumentation (2-arg: this, other).
+define void @copy_assign(ptr %this, ptr %other) "copyprof-copy-assign-op"="32" {
+entry:
+  %val = load i64, ptr %other
+  store i64 %val, ptr %this
+  ret void
+}
+; CHECK-LABEL: define void @copy_assign(ptr %this, ptr %other)
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    call void @__copyprof_copy_assign_op_enter_callback(ptr %this, ptr %other, i64 32)
+; CHECK-NOT:     call void @__copyprof_copy_assign_op_enter_callback
+; CHECK:         call void @__copyprof_copy_assign_op_exit_callback(ptr %this, ptr %other, i64 32)
+; CHECK-NEXT:    ret void
+
+;; Tests destructor instrumentation (1-arg: this).
+define void @dtor(ptr %this) "copyprof-dtor"="24" {
+entry:
+  ret void
+}
+; CHECK-LABEL: define void @dtor(ptr %this)
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    call void @__copyprof_dtor_enter_callback(ptr %this, i64 24)
+; CHECK-NOT:     call void @__copyprof_dtor_enter_callback
+; CHECK:         call void @__copyprof_dtor_exit_callback(ptr %this, i64 24)
+; CHECK-NEXT:    ret void
+
+;; Tests that exit callbacks are inserted before multiple return instructions.
+define void @ctor_multi_ret(ptr %this, i1 %cond) "copyprof-ctor"="8" {
+entry:
+  br i1 %cond, label %then, label %else
+then:
+  ret void
+else:
+  ret void
+}
+; CHECK-LABEL: define void @ctor_multi_ret(ptr %this, i1 %cond)
+; CHECK:         call void @__copyprof_ctor_enter_callback(ptr %this, i64 8)
+; CHECK-NOT:     call void @__copyprof_ctor_enter_callback
+; CHECK:       then:
+; CHECK:         call void @__copyprof_ctor_exit_callback(ptr %this, i64 8)
+; CHECK-NEXT:    ret void
+; CHECK:       else:
+; CHECK:         call void @__copyprof_ctor_exit_callback(ptr %this, i64 8)
+; CHECK-NEXT:    ret void
+
+;; Tests that exit callbacks are inserted before resume instructions
+;; (exception unwinding).
+define void @ctor_with_resume(ptr %this) "copyprof-ctor"="8" personality ptr @__gxx_personality_v0 {
+entry:
+  invoke void @may_throw() to label %cont unwind label %lpad
+cont:
+  ret void
+lpad:
+  %lp = landingpad { ptr, i32 } cleanup
+  resume { ptr, i32 } %lp
+}
+
+declare void @may_throw()
+declare i32 @__gxx_personality_v0(...)
+
+; CHECK-LABEL: define void @ctor_with_resume(ptr %this)
+; CHECK:         call void @__copyprof_ctor_enter_callback(ptr %this, i64 8)
+; CHECK:       cont:
+; CHECK:         call void @__copyprof_ctor_exit_callback(ptr %this, i64 8)
+; CHECK-NEXT:    ret void
+; CHECK:       lpad:
+; CHECK:         call void @__copyprof_ctor_exit_callback(ptr %this, i64 8)
+; CHECK-NEXT:    resume
+
+;; Tests that entry callbacks are inserted after alloca instructions.
+define void @ctor_with_alloca(ptr %this) "copyprof-ctor"="8" {
+entry:
+  %tmp = alloca i32
+  store i32 0, ptr %tmp
+  ret void
+}
+; CHECK-LABEL: define void @ctor_with_alloca(ptr %this)
+; CHECK:         %tmp = alloca i32
+; CHECK-NEXT:    call void @__copyprof_ctor_enter_callback(ptr %this, i64 8)
+; CHECK:         call void @__copyprof_ctor_exit_callback(ptr %this, i64 8)
+; CHECK-NEXT:    ret void
+
+;; Tests that no exit callback is inserted before unreachable terminators.
+define void @ctor_with_unreachable(ptr %this) "copyprof-ctor"="8" {
+entry:
+  call void @may_throw()
+  unreachable
+}
+; CHECK-LABEL: define void @ctor_with_unreachable(ptr %this)
+; CHECK:         call void @__copyprof_ctor_enter_callback(ptr %this, i64 8)
+; CHECK-NOT:     call void @__copyprof_ctor_exit_callback
+; CHECK:         unreachable
+
+;; Verifies that the module constructor calls the init function.
+; CHECK: define internal void @copyprof.module_ctor()
+; CHECK:   call void @__copyprof_init()
diff --git a/llvm/test/Instrumentation/CopyProf/no-instrumentation.ll b/llvm/test/Instrumentation/CopyProf/no-instrumentation.ll
new file mode 100644
index 0000000000000..d56f990547f8f
--- /dev/null
+++ b/llvm/test/Instrumentation/CopyProf/no-instrumentation.ll
@@ -0,0 +1,66 @@
+; Tests that CopyProf skips functions that should not be instrumented.
+;
+; RUN: opt < %s -passes='function(copyprof)' -S | FileCheck %s
+; RUN: opt < %s -passes='function(copyprof-stores)' -S | FileCheck %s --check-prefix=STORES
+
+target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"
+target triple = "x86_64-unknown-linux-gnu"
+
+;; Tests that a function without copyprof attributes is not instrumented.
+define void @no_attrs(ptr %this) {
+entry:
+  ret void
+}
+; CHECK-LABEL: define void @no_attrs(ptr %this)
+; CHECK-NOT:     call void @__copyprof_
+; CHECK:         ret void
+; STORES-LABEL: define void @no_attrs(ptr %this)
+; STORES-NOT:     call void @__copyprof_store_callback
+; STORES:         ret void
+
+;; Tests that instrumentation is skipped when disable_sanitizer_instrumentation is present.
+define void @disabled_ctor(ptr %this) "copyprof-ctor"="8" disable_sanitizer_instrumentation {
+entry:
+  ret void
+}
+; CHECK-LABEL: define void @disabled_ctor(ptr %this)
+; CHECK-NOT:     call void @__copyprof_
+; CHECK:         ret void
+
+;; Tests that store instrumentation is skipped when disable_sanitizer_instrumentation is present.
+define void @disabled_ctor_with_store(ptr %this) "copyprof-ctor"="8" disable_sanitizer_instrumentation {
+entry:
+  store i32 0, ptr %this
+  ret void
+}
+; CHECK-LABEL: define void @disabled_ctor_with_store(ptr %this)
+; CHECK-NOT:     call void @__copyprof_
+; CHECK:         ret void
+; STORES-LABEL: define void @disabled_ctor_with_store(ptr %this)
+; STORES-NOT:     call void @__copyprof_store_callback
+; STORES:         store i32 0, ptr %this
+; STORES-NEXT:    ret void
+
+;; Tests that store instrumentation is skipped for regular functions when disable_sanitizer_instrumentation
+;; is present.
+define void @disabled_stores(ptr %a) disable_sanitizer_instrumentation {
+entry:
+  store i32 42, ptr %a
+  ret void
+}
+; STORES-LABEL: define void @disabled_stores(ptr %a)
+; STORES-NOT:     call void @__copyprof_store_callback
+; STORES:         store i32 42, ptr %a
+; STORES-NEXT:    ret void
+
+;; Tests that the module constructor itself is not instrumented by either pass.
+define internal void @copyprof.module_ctor() {
+entry:
+  ret void
+}
+; CHECK-LABEL: define internal void @copyprof.module_ctor()
+; CHECK-NOT:     call void @__copyprof_
+; CHECK:         ret void
+; STORES-LABEL: define internal void @copyprof.module_ctor()
+; STORES-NOT:     call void @__copyprof_store_callback
+; STORES:         ret void
diff --git a/llvm/test/Instrumentation/CopyProf/store-instrumentation.ll b/llvm/test/Instrumentation/CopyProf/store-instrumentation.ll
new file mode 100644
index 0000000000000..0370a26073292
--- /dev/null
+++ b/llvm/test/Instrumentation/CopyProf/store-instrumentation.ll
@@ -0,0 +1,101 @@
+; Tests CopyProf store instrumentation.
+;
+; RUN: opt < %s -passes='function(copyprof-stores)' -S | FileCheck %s
+
+target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"
+target triple = "x86_64-unknown-linux-gnu"
+
+;; Tests that a simple i32 store is instrumented.
+define void @test_store_i32(ptr %a) {
+entry:
+  store i32 42, ptr %a, align 4
+  ret void
+}
+; CHECK-LABEL: define void @test_store_i32(ptr %a)
+; CHECK:         call void @__copyprof_store_callback(ptr %a, i64 4)
+; CHECK-NEXT:    store i32 42, ptr %a
+
+;; Tests that an i64 store is instrumented with the correct size.
+define void @test_store_i64(ptr %a) {
+entry:
+  store i64 100, ptr %a, align 8
+  ret void
+}
+; CHECK-LABEL: define void @test_store_i64(ptr %a)
+; CHECK:         call void @__copyprof_store_callback(ptr %a, i64 8)
+; CHECK-NEXT:    store i64 100, ptr %a
+
+;; Tests that multiple stores in the same function are all instrumented.
+define void @test_multiple_stores(ptr %a, ptr %b) {
+entry:
+  store i32 1, ptr %a, align 4
+  store i32 2, ptr %b, align 4
+  ret void
+}
+; CHECK-LABEL: define void @test_multiple_stores(ptr %a, ptr %b)
+; CHECK:         call void @__copyprof_store_callback(ptr %a, i64 4)
+; CHECK-NEXT:    store i32 1, ptr %a
+; CHECK:         call void @__copyprof_store_callback(ptr %b, i64 4)
+; CHECK-NEXT:    store i32 2, ptr %b
+
+;; Tests that a function with no stores is not modified.
+define i32 @test_no_stores(ptr %a) {
+entry:
+  %val = load i32, ptr %a
+  ret i32 %val
+}
+; CHECK-LABEL: define i32 @test_no_stores(ptr %a)
+; CHECK-NOT:     call void @__copyprof_store_callback
+; CHECK:         ret i32
+
+;; Tests that stores to non-default address spaces are not instrumented.
+define void @test_addrspace_store(ptr addrspace(1) %a) {
+entry:
+  store i32 42, ptr addrspace(1) %a, align 4
+  ret void
+}
+; CHECK-LABEL: define void @test_addrspace_store(ptr addrspace(1) %a)
+; CHECK-NOT:     call void @__copyprof_store_callback
+; CHECK:         store i32 42, ptr addrspace(1) %a
+; CHECK-NEXT:    ret void
+
+;; Tests that a vector store is instrumented with the correct aggregate size.
+define void @test_store_vector(ptr %a) {
+entry:
+  store <4 x i32> zeroinitializer, ptr %a, align 16
+  ret void
+}
+; CHECK-LABEL: define void @test_store_vector(ptr %a)
+; CHECK:         call void @__copyprof_store_callback(ptr %a, i64 16)
+; CHECK-NEXT:    store <4 x i32> zeroinitializer, ptr %a
+
+;; Tests that an aggregate store is instrumented with the correct size.
+define void @test_store_struct(ptr %a) {
+entry:
+  store <{ i32, i16, i8 }> zeroinitializer, ptr %a
+  ret void
+}
+; CHECK-LABEL: define void @test_store_struct(ptr %a)
+; CHECK:         call void @__copyprof_store_callback(ptr %a, i64 7)
+; CHECK-NEXT:    store <{ i32, i16, i8 }> zeroinitializer, ptr %a
+
+;; Tests that volatile stores are instrumented.
+define void @test_store_volatile(ptr %a) {
+entry:
+  store volatile i32 42, ptr %a, align 4
+  ret void
+}
+; CHECK-LABEL: define void @test_store_volatile(ptr %a)
+; CHECK:         call void @__copyprof_store_callback(ptr %a, i64 4)
+; CHECK-NEXT:    store volatile i32 42, ptr %a
+
+;; Tests that atomic stores are instrumented.
+define void @test_store_atomic(ptr %a) {
+entry:
+  store atomic i32 42, ptr %a monotonic, align 4
+  ret void
+}
+; CHECK-LABEL: define void @test_store_atomic(ptr %a)
+; CHECK:         call void @__copyprof_store_callback(ptr %a, i64 4)
+; CHECK-NEXT:    store atomic i32 42, ptr %a monotonic
+

>From 1ca49ae8119971239393f5260237f10ee4318305 Mon Sep 17 00:00:00 2001
From: Jan Newger <jannewger at gmail.com>
Date: Fri, 3 Jul 2026 16:52:32 +0000
Subject: [PATCH 2/5] fixup! [CopyProf] Add CopyProf instrumentation passes.

---
 llvm/include/llvm/Transforms/Instrumentation/CopyProf.h | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/llvm/include/llvm/Transforms/Instrumentation/CopyProf.h b/llvm/include/llvm/Transforms/Instrumentation/CopyProf.h
index 73c49024c52f5..5755b4ccd693b 100644
--- a/llvm/include/llvm/Transforms/Instrumentation/CopyProf.h
+++ b/llvm/include/llvm/Transforms/Instrumentation/CopyProf.h
@@ -15,6 +15,7 @@
 #define LLVM_TRANSFORMS_INSTRUMENTATION_COPYPROF_H
 
 #include "llvm/IR/PassManager.h"
+
 namespace llvm {
 
 // Early-stage pass that instruments special member functions to call into the
@@ -22,7 +23,7 @@ namespace llvm {
 class CopyProfPass : public PassInfoMixin<CopyProfPass> {
 public:
   CopyProfPass() = default;
-  PreservedAnalyses run(Function &F, FunctionAnalysisManager &FAM);
+  LLVM_ABI PreservedAnalyses run(Function &F, FunctionAnalysisManager &FAM);
 
   static bool isRequired() { return true; }
 };
@@ -32,7 +33,7 @@ class CopyProfPass : public PassInfoMixin<CopyProfPass> {
 class ModuleCopyProfPass : public PassInfoMixin<ModuleCopyProfPass> {
 public:
   ModuleCopyProfPass() = default;
-  PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);
+  LLVM_ABI PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);
 
   static bool isRequired() { return true; }
 };
@@ -42,7 +43,7 @@ class ModuleCopyProfPass : public PassInfoMixin<ModuleCopyProfPass> {
 class CopyProfStoresPass : public PassInfoMixin<CopyProfStoresPass> {
 public:
   CopyProfStoresPass() = default;
-  PreservedAnalyses run(Function &F, FunctionAnalysisManager &FAM);
+  LLVM_ABI PreservedAnalyses run(Function &F, FunctionAnalysisManager &FAM);
 
   static bool isRequired() { return true; }
 };

>From 305b631ca8d028fda3cc400152851a6db545a422 Mon Sep 17 00:00:00 2001
From: Jan Newger <jannewger at gmail.com>
Date: Sun, 5 Jul 2026 22:18:47 +0200
Subject: [PATCH 3/5] fixup! [CopyProf] Add CopyProf instrumentation passes.

Skip adding callbacks in musttail functions, skip naked functions,
more robust module-ctor exclusion, skip scalable-vector store instrumentation,
avoid adding CopyProf declarations to each module even if no instrumentation
emitted.
---
 .../Transforms/Instrumentation/CopyProf.cpp   | 57 ++++++++++++++-----
 .../CopyProf/function-instrumentation.ll      | 18 +++++-
 .../CopyProf/no-instrumentation.ll            | 14 +++++
 .../CopyProf/store-instrumentation.ll         | 12 ++++
 4 files changed, 85 insertions(+), 16 deletions(-)

diff --git a/llvm/lib/Transforms/Instrumentation/CopyProf.cpp b/llvm/lib/Transforms/Instrumentation/CopyProf.cpp
index 6efc318b20195..553d3aef7c8ec 100644
--- a/llvm/lib/Transforms/Instrumentation/CopyProf.cpp
+++ b/llvm/lib/Transforms/Instrumentation/CopyProf.cpp
@@ -82,12 +82,43 @@ static bool insertModuleCtor(Module &M) {
       M, CopyProfModuleCtorName, CopyProfInitName,
       /*InitArgTypes=*/{},
       /*InitArgs=*/{}, [&](Function *Ctor, FunctionCallee) {
+        // Mark the ctor so it's never instrumented itself.
+        Ctor->addFnAttr(Attribute::DisableSanitizerInstrumentation);
         appendToGlobalCtors(M, Ctor, 0);
         Modified = true;
       });
   return Modified;
 }
 
+static bool isCopyProfCandidate(const Function &F) {
+  // Must not instrument functions that are explicitly disallowed for
+  // instrumentation, or naked functions.
+  if (F.isDeclaration() ||
+      F.hasFnAttribute(Attribute::DisableSanitizerInstrumentation) ||
+      F.hasFnAttribute(Attribute::Naked))
+    return false;
+
+  // Don't instrument a function at all if it's ending with a tail call.
+  // Alternatively, the exit callback could be placed before the tail call, but
+  // that would risk missing observable side-effects needed by CopyProf to infer
+  // memory ownership (potentially leading to flase positive reports).
+  // Skipping this function favors false negatives over false positives.
+  for (const BasicBlock &BB : F)
+    if (BB.getTerminatingMustTailCall())
+      return false;
+
+  return F.hasFnAttribute(CopyProfCtorAttr) ||
+         F.hasFnAttribute(CopyProfCopyCtorAttr) ||
+         F.hasFnAttribute(CopyProfCopyAssignAttr) ||
+         F.hasFnAttribute(CopyProfDtorAttr);
+}
+
+static bool isCopyProfStoresCandidate(const Function &F) {
+  return !F.isDeclaration() &&
+         !F.hasFnAttribute(Attribute::DisableSanitizerInstrumentation) &&
+         !F.hasFnAttribute(Attribute::Naked);
+}
+
 // Returns the object size in bytes that was stored in the given function
 // attribute during parsing in the frontend.
 static size_t getAttrValueAsInt(const Function &F, StringRef Attr) {
@@ -170,13 +201,6 @@ CopyProf::CopyProf(Module &M) {
 }
 
 bool CopyProf::instrumentFunction(Function &F) {
-  // Must not instrument our own module c'tor or functions that are explicitly
-  // disallowed for instrumentation.
-  if (F.isDeclaration() ||
-      F.hasFnAttribute(Attribute::DisableSanitizerInstrumentation) ||
-      F.getName() == CopyProfModuleCtorName)
-    return false;
-
   bool Modified = true;
   if (F.hasFnAttribute(CopyProfCtorAttr))
     insertCallback(F, getAttrValueAsInt(F, CopyProfCtorAttr), /*NumArgs=*/1,
@@ -242,28 +266,27 @@ CopyProfStores::CopyProfStores(Module &M) {
 }
 
 bool CopyProfStores::instrumentFunction(Function &F) {
-  if (F.isDeclaration() ||
-      F.hasFnAttribute(Attribute::DisableSanitizerInstrumentation) ||
-      F.getName() == CopyProfModuleCtorName)
-    return false;
-
   // TODO: handle all types of memory stores (memory intrinsics, masked store
   // intrinsics, AtomicRMW, and AtomicCmpXchg).
+  const DataLayout &DL = F.getParent()->getDataLayout();
   SmallVector<StoreInst *, 16> ToInstrument;
   for (BasicBlock &BB : F) {
     for (Instruction &I : BB) {
       if (auto *SI = dyn_cast<StoreInst>(&I);
           SI != nullptr && SI->getPointerAddressSpace() == 0 &&
-          !SI->hasMetadata(LLVMContext::MD_nosanitize))
+          !SI->hasMetadata(LLVMContext::MD_nosanitize) &&
+          // Scalable vector stores have no compile-time-constant size so skip
+          // them.
+          !DL.getTypeStoreSize(SI->getValueOperand()->getType()).isScalable())
         ToInstrument.push_back(SI);
     }
   }
   if (ToInstrument.empty())
     return false;
 
-  const DataLayout &DL = F.getParent()->getDataLayout();
   for (StoreInst *SI : ToInstrument) {
-    uint64_t StoredSize = DL.getTypeStoreSize(SI->getValueOperand()->getType());
+    uint64_t StoredSize =
+        DL.getTypeStoreSize(SI->getValueOperand()->getType()).getFixedValue();
     InstrumentationIRBuilder IRB(SI);
     std::array<Value *, 2> Args = {SI->getPointerOperand(),
                                    ConstantInt::get(IntPtrTy, StoredSize)};
@@ -273,6 +296,8 @@ bool CopyProfStores::instrumentFunction(Function &F) {
 }
 
 PreservedAnalyses CopyProfPass::run(Function &F, FunctionAnalysisManager &) {
+  if (!isCopyProfCandidate(F))
+    return PreservedAnalyses::all();
   CopyProf Impl(*F.getParent());
   if (!Impl.instrumentFunction(F))
     return PreservedAnalyses::all();
@@ -288,6 +313,8 @@ PreservedAnalyses ModuleCopyProfPass::run(Module &M, ModuleAnalysisManager &) {
 
 PreservedAnalyses CopyProfStoresPass::run(Function &F,
                                           FunctionAnalysisManager &) {
+  if (!isCopyProfStoresCandidate(F))
+    return PreservedAnalyses::all();
   CopyProfStores Impl(*F.getParent());
   if (!Impl.instrumentFunction(F))
     return PreservedAnalyses::all();
diff --git a/llvm/test/Instrumentation/CopyProf/function-instrumentation.ll b/llvm/test/Instrumentation/CopyProf/function-instrumentation.ll
index dd69dd7914250..d1a8b667c123a 100644
--- a/llvm/test/Instrumentation/CopyProf/function-instrumentation.ll
+++ b/llvm/test/Instrumentation/CopyProf/function-instrumentation.ll
@@ -106,6 +106,20 @@ declare i32 @__gxx_personality_v0(...)
 ; CHECK:         call void @__copyprof_ctor_exit_callback(ptr %this, i64 8)
 ; CHECK-NEXT:    resume
 
+;; Tests that a function containing a musttail call is skipped entirely.
+declare void @tail_callee(ptr)
+
+define void @ctor_with_musttail(ptr %this) "copyprof-ctor"="8" {
+entry:
+  musttail call void @tail_callee(ptr %this)
+  ret void
+}
+; CHECK-LABEL: define void @ctor_with_musttail(ptr %this)
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    musttail call void @tail_callee(ptr %this)
+; CHECK-NEXT:    ret void
+; CHECK-NOT:     call void @__copyprof_
+
 ;; Tests that entry callbacks are inserted after alloca instructions.
 define void @ctor_with_alloca(ptr %this) "copyprof-ctor"="8" {
 entry:
@@ -130,6 +144,8 @@ entry:
 ; CHECK-NOT:     call void @__copyprof_ctor_exit_callback
 ; CHECK:         unreachable
 
-;; Verifies that the module constructor calls the init function.
+;; Verifies that the module constructor calls the init function and is marked so
+;; that it's never instrumented itself.
 ; CHECK: define internal void @copyprof.module_ctor()
 ; CHECK:   call void @__copyprof_init()
+; CHECK: disable_sanitizer_instrumentation
diff --git a/llvm/test/Instrumentation/CopyProf/no-instrumentation.ll b/llvm/test/Instrumentation/CopyProf/no-instrumentation.ll
index d56f990547f8f..b7950771de503 100644
--- a/llvm/test/Instrumentation/CopyProf/no-instrumentation.ll
+++ b/llvm/test/Instrumentation/CopyProf/no-instrumentation.ll
@@ -53,6 +53,20 @@ entry:
 ; STORES:         store i32 42, ptr %a
 ; STORES-NEXT:    ret void
 
+;; Tests that instrumentation is skipped for naked functions.
+define void @naked_ctor_with_store(ptr %this) "copyprof-ctor"="8" naked {
+entry:
+  store i32 0, ptr null
+  ret void
+}
+; CHECK-LABEL: define void @naked_ctor_with_store(ptr %this)
+; CHECK-NOT:     call void @__copyprof_
+; CHECK:         ret void
+; STORES-LABEL: define void @naked_ctor_with_store(ptr %this)
+; STORES-NOT:     call void @__copyprof_store_callback
+; STORES:         store i32 0, ptr null
+; STORES-NEXT:    ret void
+
 ;; Tests that the module constructor itself is not instrumented by either pass.
 define internal void @copyprof.module_ctor() {
 entry:
diff --git a/llvm/test/Instrumentation/CopyProf/store-instrumentation.ll b/llvm/test/Instrumentation/CopyProf/store-instrumentation.ll
index 0370a26073292..f5a7ce414f850 100644
--- a/llvm/test/Instrumentation/CopyProf/store-instrumentation.ll
+++ b/llvm/test/Instrumentation/CopyProf/store-instrumentation.ll
@@ -59,6 +59,18 @@ entry:
 ; CHECK:         store i32 42, ptr addrspace(1) %a
 ; CHECK-NEXT:    ret void
 
+;; Tests that scalable vector stores are skipped as they have no
+;; compile-time-constant store size.
+define void @test_store_scalable_vector(ptr %a) {
+entry:
+  store <vscale x 4 x i32> zeroinitializer, ptr %a, align 16
+  ret void
+}
+; CHECK-LABEL: define void @test_store_scalable_vector(ptr %a)
+; CHECK-NOT:     call void @__copyprof_store_callback
+; CHECK:         store <vscale x 4 x i32> zeroinitializer, ptr %a
+; CHECK-NEXT:    ret void
+
 ;; Tests that a vector store is instrumented with the correct aggregate size.
 define void @test_store_vector(ptr %a) {
 entry:

>From 6f0696f68ce352ba4b16c6a275e2a2d4bcf3fefe Mon Sep 17 00:00:00 2001
From: Jan Newger <jannewger at gmail.com>
Date: Fri, 24 Jul 2026 12:45:20 +0000
Subject: [PATCH 4/5] fixup! fixup! [CopyProf] Add CopyProf instrumentation
 passes.

---
 .../Transforms/Instrumentation/CopyProf.cpp   | 45 ++++++++++---------
 1 file changed, 24 insertions(+), 21 deletions(-)

diff --git a/llvm/lib/Transforms/Instrumentation/CopyProf.cpp b/llvm/lib/Transforms/Instrumentation/CopyProf.cpp
index 553d3aef7c8ec..61eed4cdaeeec 100644
--- a/llvm/lib/Transforms/Instrumentation/CopyProf.cpp
+++ b/llvm/lib/Transforms/Instrumentation/CopyProf.cpp
@@ -32,8 +32,6 @@
 #include "llvm/IR/Module.h"
 #include "llvm/IR/PassManager.h"
 #include "llvm/Support/Casting.h"
-#include "llvm/Support/ErrorHandling.h"
-#include "llvm/Support/FormatVariadic.h"
 #include "llvm/Transforms/Utils/Instrumentation.h"
 #include "llvm/Transforms/Utils/ModuleUtils.h"
 #include <array>
@@ -98,19 +96,25 @@ static bool isCopyProfCandidate(const Function &F) {
       F.hasFnAttribute(Attribute::Naked))
     return false;
 
-  // Don't instrument a function at all if it's ending with a tail call.
+  if (!F.hasFnAttribute(CopyProfCtorAttr) &&
+      !F.hasFnAttribute(CopyProfCopyCtorAttr) &&
+      !F.hasFnAttribute(CopyProfCopyAssignAttr) &&
+      !F.hasFnAttribute(CopyProfDtorAttr))
+    return false;
+
+  // Don't instrument a function at all if it ends in a tail call.
   // Alternatively, the exit callback could be placed before the tail call, but
   // that would risk missing observable side-effects needed by CopyProf to infer
-  // memory ownership (potentially leading to flase positive reports).
-  // Skipping this function favors false negatives over false positives.
+  // memory ownership (potentially leading to false positive reports).
+  // For example, if the tail would deallocate memory then CopyProf would be
+  // unable to inspect that memory and the object could be misclassified as
+  // having been unnecessarily copied. Skipping functions ending in musttail
+  // calls therefore favors false negatives over false positives.
   for (const BasicBlock &BB : F)
     if (BB.getTerminatingMustTailCall())
       return false;
 
-  return F.hasFnAttribute(CopyProfCtorAttr) ||
-         F.hasFnAttribute(CopyProfCopyCtorAttr) ||
-         F.hasFnAttribute(CopyProfCopyAssignAttr) ||
-         F.hasFnAttribute(CopyProfDtorAttr);
+  return true;
 }
 
 static bool isCopyProfStoresCandidate(const Function &F) {
@@ -123,13 +127,11 @@ static bool isCopyProfStoresCandidate(const Function &F) {
 // attribute during parsing in the frontend.
 static size_t getAttrValueAsInt(const Function &F, StringRef Attr) {
   size_t IntValue = 0;
-  if (!to_integer<size_t>(F.getFnAttribute(Attr).getValueAsString(), IntValue,
-                          /*Base=*/10)) {
-    report_fatal_error(formatv("Unable to parse integer value from function "
-                               "attribute value in '{0}': {1}:{2}",
-                               F.getName(), Attr,
-                               F.getFnAttribute(Attr).getValueAsString()));
-  }
+  [[maybe_unused]] bool Success =
+      to_integer<size_t>(F.getFnAttribute(Attr).getValueAsString(), IntValue,
+                         /*Base=*/10);
+  assert(Success &&
+         "Unable to parse object size from CopyProf function attribute value.");
   return IntValue;
 }
 
@@ -145,7 +147,6 @@ class CopyProf {
   void insertCallback(Function &F, size_t ObjSize, unsigned NumArgs,
                       FunctionCallee Callback, FunctionCallee ExitCallback);
 
-  LLVMContext *Ctx;
   Type *IntPtrTy;
   FunctionCallee CtorEnterCallback;
   FunctionCallee CtorExitCallback;
@@ -172,14 +173,14 @@ class CopyProfStores {
 } // namespace
 
 CopyProf::CopyProf(Module &M) {
-  Ctx = &M.getContext();
-  IRBuilder<> IRB(*Ctx);
+  LLVMContext &Ctx = M.getContext();
+  IRBuilder<> IRB(Ctx);
   IntPtrTy = IRB.getIntPtrTy(M.getDataLayout());
   Type *PtrTy = IRB.getPtrTy();
   Type *VoidTy = IRB.getVoidTy();
   // CopyProf callbacks never throw exceptions.
   AttributeList Attr;
-  Attr = Attr.addFnAttribute(*Ctx, Attribute::NoUnwind);
+  Attr = Attr.addFnAttribute(Ctx, Attribute::NoUnwind);
   CtorEnterCallback = M.getOrInsertFunction(CopyProfCtorEnterCallbackName, Attr,
                                             VoidTy, PtrTy, IntPtrTy);
   CtorExitCallback = M.getOrInsertFunction(CopyProfCtorExitCallbackName, Attr,
@@ -266,8 +267,10 @@ CopyProfStores::CopyProfStores(Module &M) {
 }
 
 bool CopyProfStores::instrumentFunction(Function &F) {
-  // TODO: handle all types of memory stores (memory intrinsics, masked store
+  // TODO: Handle all types of memory stores (memory intrinsics, masked store
   // intrinsics, AtomicRMW, and AtomicCmpXchg).
+  // TODO: Skip stores to alloca if only made of fundamental types, arrays
+  // thereof and (possibly) class types that are trivial and aggregate.
   const DataLayout &DL = F.getParent()->getDataLayout();
   SmallVector<StoreInst *, 16> ToInstrument;
   for (BasicBlock &BB : F) {

>From f630810d53b1fffd4796ee8286a94d62d8afe9f1 Mon Sep 17 00:00:00 2001
From: Jan Newger <jannewger at gmail.com>
Date: Fri, 24 Jul 2026 16:37:23 +0000
Subject: [PATCH 5/5] fixup! fixup! fixup! [CopyProf] Add CopyProf
 instrumentation passes.

---
 .../CopyProf/function-instrumentation.ll           | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/llvm/test/Instrumentation/CopyProf/function-instrumentation.ll b/llvm/test/Instrumentation/CopyProf/function-instrumentation.ll
index d1a8b667c123a..96854ecf18022 100644
--- a/llvm/test/Instrumentation/CopyProf/function-instrumentation.ll
+++ b/llvm/test/Instrumentation/CopyProf/function-instrumentation.ll
@@ -120,6 +120,20 @@ entry:
 ; CHECK-NEXT:    ret void
 ; CHECK-NOT:     call void @__copyprof_
 
+;; Tests that a function containing an ordinary tail call is instrumented normally,
+;; with the exit callback inserted between the tail call and return instructions.
+define void @ctor_with_tail(ptr %this) "copyprof-ctor"="8" {
+entry:
+  tail call void @tail_callee(ptr %this)
+  ret void
+}
+; CHECK-LABEL: define void @ctor_with_tail(ptr %this)
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    call void @__copyprof_ctor_enter_callback(ptr %this, i64 8)
+; CHECK-NEXT:    tail call void @tail_callee(ptr %this)
+; CHECK-NEXT:    call void @__copyprof_ctor_exit_callback(ptr %this, i64 8)
+; CHECK-NEXT:    ret void
+
 ;; Tests that entry callbacks are inserted after alloca instructions.
 define void @ctor_with_alloca(ptr %this) "copyprof-ctor"="8" {
 entry:



More information about the llvm-commits mailing list