[compiler-rt] [CopyProf] Implement minimal CopyProf runtime. (PR #223458)

via llvm-commits llvm-commits at lists.llvm.org
Tue Sep 15 12:00:57 PDT 2026


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

>From 6cb39ea3ad8782c3d536fd10d9dbccfa8a27a2a7 Mon Sep 17 00:00:00 2001
From: jannewger <jannewger at google.com>
Date: Tue, 23 Jun 2026 18:11:57 +0000
Subject: [PATCH 1/2] [CopyProf] Implement minimal CopyProf runtime.

The runtime tracks object copies using shadow memory (with a 1:8 compression scale).

Each thread uses TLS to keep track of whether execution is currently inside a special
member function. Copy function mark corresponding shadow memory as a copy.
Upon destruction, if an object was marked as a copy and never modified, it is
classified as an unnecessary copy, and a report with a stack trace is generated.
---
 compiler-rt/CMakeLists.txt                    |   2 +
 .../cmake/Modules/AllSupportedArchDefs.cmake  |   1 +
 compiler-rt/cmake/config-ix.cmake             |   9 +
 compiler-rt/lib/CMakeLists.txt                |   8 +-
 compiler-rt/lib/copyprof/.clang-format        |   3 +
 compiler-rt/lib/copyprof/CMakeLists.txt       |  64 +++++++
 compiler-rt/lib/copyprof/copyprof.cpp         | 156 +++++++++++++++
 compiler-rt/lib/copyprof/copyprof.syms.extra  |   1 +
 .../copyprof/copyprof_interface_internal.h    |  50 +++++
 compiler-rt/lib/copyprof/copyprof_internal.h  |  33 ++++
 .../lib/copyprof/copyprof_reporting.cpp       |  42 +++++
 compiler-rt/lib/copyprof/copyprof_reporting.h |  28 +++
 compiler-rt/lib/copyprof/copyprof_shadow.cpp  | 112 +++++++++++
 compiler-rt/lib/copyprof/copyprof_shadow.h    |  63 +++++++
 compiler-rt/lib/copyprof/copyprof_stack.cpp   |  24 +++
 compiler-rt/lib/copyprof/copyprof_state.cpp   |  20 ++
 compiler-rt/lib/copyprof/copyprof_state.h     |  70 +++++++
 compiler-rt/lib/copyprof/tests/CMakeLists.txt |  73 +++++++
 .../copyprof/tests/copyprof_shadow_test.cpp   | 164 ++++++++++++++++
 .../copyprof/tests/copyprof_state_test.cpp    | 178 ++++++++++++++++++
 compiler-rt/lib/copyprof/tests/driver.cpp     |  19 ++
 .../sanitizer_internal_defs.h                 |   3 +
 22 files changed, 1121 insertions(+), 2 deletions(-)
 create mode 100644 compiler-rt/lib/copyprof/.clang-format
 create mode 100644 compiler-rt/lib/copyprof/CMakeLists.txt
 create mode 100644 compiler-rt/lib/copyprof/copyprof.cpp
 create mode 100644 compiler-rt/lib/copyprof/copyprof.syms.extra
 create mode 100644 compiler-rt/lib/copyprof/copyprof_interface_internal.h
 create mode 100644 compiler-rt/lib/copyprof/copyprof_internal.h
 create mode 100644 compiler-rt/lib/copyprof/copyprof_reporting.cpp
 create mode 100644 compiler-rt/lib/copyprof/copyprof_reporting.h
 create mode 100644 compiler-rt/lib/copyprof/copyprof_shadow.cpp
 create mode 100644 compiler-rt/lib/copyprof/copyprof_shadow.h
 create mode 100644 compiler-rt/lib/copyprof/copyprof_stack.cpp
 create mode 100644 compiler-rt/lib/copyprof/copyprof_state.cpp
 create mode 100644 compiler-rt/lib/copyprof/copyprof_state.h
 create mode 100644 compiler-rt/lib/copyprof/tests/CMakeLists.txt
 create mode 100644 compiler-rt/lib/copyprof/tests/copyprof_shadow_test.cpp
 create mode 100644 compiler-rt/lib/copyprof/tests/copyprof_state_test.cpp
 create mode 100644 compiler-rt/lib/copyprof/tests/driver.cpp

diff --git a/compiler-rt/CMakeLists.txt b/compiler-rt/CMakeLists.txt
index 9e45a43b34287d..0458b17ba38c20 100644
--- a/compiler-rt/CMakeLists.txt
+++ b/compiler-rt/CMakeLists.txt
@@ -92,6 +92,8 @@ option(COMPILER_RT_BUILD_CTX_PROFILE "Build ctx profile runtime" ON)
 mark_as_advanced(COMPILER_RT_BUILD_CTX_PROFILE)
 option(COMPILER_RT_BUILD_MEMPROF "Build memory profiling runtime" ON)
 mark_as_advanced(COMPILER_RT_BUILD_MEMPROF)
+option(COMPILER_RT_BUILD_COPYPROF "Build copy profiling runtime" ON)
+mark_as_advanced(COMPILER_RT_BUILD_COPYPROF)
 option(COMPILER_RT_BUILD_XRAY_NO_PREINIT "Build xray with no preinit patching" OFF)
 mark_as_advanced(COMPILER_RT_BUILD_XRAY_NO_PREINIT)
 option(COMPILER_RT_BUILD_ORC "Build ORC runtime" ON)
diff --git a/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake b/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake
index 9c9874d94a1f2d..8956ed2d305de5 100644
--- a/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake
+++ b/compiler-rt/cmake/Modules/AllSupportedArchDefs.cmake
@@ -118,6 +118,7 @@ set(ALL_CFI_SUPPORTED_ARCH ${X86} ${X86_64} ${ARM32} ${ARM64} ${MIPS64}
     ${HEXAGON} ${LOONGARCH64})
 set(ALL_SCUDO_STANDALONE_SUPPORTED_ARCH ${X86} ${X86_64} ${ARM32} ${ARM64}
     ${MIPS32} ${MIPS64} ${PPC64} ${HEXAGON} ${LOONGARCH64} ${RISCV64} ${S390X})
+set(ALL_COPYPROF_SUPPORTED_ARCH ${X86_64})
 if(APPLE)
 set(ALL_XRAY_SUPPORTED_ARCH ${X86_64} ${ARM64})
 else()
diff --git a/compiler-rt/cmake/config-ix.cmake b/compiler-rt/cmake/config-ix.cmake
index 34a00a13f5dc08..7773c3b6bbd714 100644
--- a/compiler-rt/cmake/config-ix.cmake
+++ b/compiler-rt/cmake/config-ix.cmake
@@ -740,6 +740,8 @@ else()
   filter_available_targets(GWP_ASAN_SUPPORTED_ARCH ${ALL_GWP_ASAN_SUPPORTED_ARCH})
   filter_available_targets(NSAN_SUPPORTED_ARCH ${ALL_NSAN_SUPPORTED_ARCH})
   filter_available_targets(ORC_SUPPORTED_ARCH ${ALL_ORC_SUPPORTED_ARCH})
+  filter_available_targets(COPYPROF_SUPPORTED_ARCH ${ALL_COPYPROF_SUPPORTED_ARCH})
+
 endif()
 
 if (MSVC)
@@ -915,6 +917,13 @@ else()
   set(COMPILER_RT_HAS_CFI FALSE)
 endif()
 
+if (COMPILER_RT_HAS_SANITIZER_COMMON AND COPYPROF_SUPPORTED_ARCH AND
+    OS_NAME MATCHES "Linux")
+  set(COMPILER_RT_HAS_COPYPROF TRUE)
+else()
+  set(COMPILER_RT_HAS_COPYPROF FALSE)
+endif()
+
 #TODO(kostyak): add back Android & Fuchsia when the code settles a bit.
 if (SCUDO_STANDALONE_SUPPORTED_ARCH AND
     COMPILER_RT_BUILD_SANITIZERS AND
diff --git a/compiler-rt/lib/CMakeLists.txt b/compiler-rt/lib/CMakeLists.txt
index a5b2fbb38762ca..90efc541cf15c1 100644
--- a/compiler-rt/lib/CMakeLists.txt
+++ b/compiler-rt/lib/CMakeLists.txt
@@ -9,7 +9,7 @@ include(SanitizerUtils)
 #
 #TODO: Refactor sanitizer_common into smaller pieces (e.g. flag parsing, utils).
 if (COMPILER_RT_HAS_SANITIZER_COMMON AND
-    (COMPILER_RT_BUILD_SANITIZERS OR COMPILER_RT_BUILD_XRAY OR COMPILER_RT_BUILD_MEMPROF OR COMPILER_RT_BUILD_CTX_PROFILE))
+    (COMPILER_RT_BUILD_SANITIZERS OR COMPILER_RT_BUILD_XRAY OR COMPILER_RT_BUILD_MEMPROF OR COMPILER_RT_BUILD_COPYPROF OR COMPILER_RT_BUILD_CTX_PROFILE))
   add_subdirectory(sanitizer_common)
 endif()
 
@@ -31,7 +31,7 @@ function(compiler_rt_build_runtime runtime)
   endif()
 endfunction()
 
-if(COMPILER_RT_BUILD_SANITIZERS OR COMPILER_RT_BUILD_MEMPROF)
+if(COMPILER_RT_BUILD_SANITIZERS OR COMPILER_RT_BUILD_MEMPROF OR COMPILER_RT_BUILD_COPYPROF)
   compiler_rt_build_runtime(interception)
 endif()
 
@@ -74,6 +74,10 @@ if(COMPILER_RT_BUILD_MEMPROF AND COMPILER_RT_HAS_SANITIZER_COMMON)
   compiler_rt_build_runtime(memprof)
 endif()
 
+if(COMPILER_RT_BUILD_COPYPROF AND COMPILER_RT_HAS_SANITIZER_COMMON)
+  compiler_rt_build_runtime(copyprof)
+endif()
+
 if(COMPILER_RT_BUILD_ORC)
   compiler_rt_build_runtime(orc)
 endif()
diff --git a/compiler-rt/lib/copyprof/.clang-format b/compiler-rt/lib/copyprof/.clang-format
new file mode 100644
index 00000000000000..1f2a97030379da
--- /dev/null
+++ b/compiler-rt/lib/copyprof/.clang-format
@@ -0,0 +1,3 @@
+BasedOnStyle: Google
+AllowShortIfStatementsOnASingleLine: false
+IndentPPDirectives: AfterHash
diff --git a/compiler-rt/lib/copyprof/CMakeLists.txt b/compiler-rt/lib/copyprof/CMakeLists.txt
new file mode 100644
index 00000000000000..1de2d5f3682b29
--- /dev/null
+++ b/compiler-rt/lib/copyprof/CMakeLists.txt
@@ -0,0 +1,64 @@
+# Build for the CopyProf runtime support library.
+
+set(COPYPROF_SOURCES
+  copyprof.cpp
+  copyprof_reporting.cpp
+  copyprof_shadow.cpp
+  copyprof_stack.cpp
+  copyprof_state.cpp
+)
+
+set(COPYPROF_HEADERS
+  copyprof_interface_internal.h
+  copyprof_internal.h
+  copyprof_reporting.h
+  copyprof_shadow.h
+  copyprof_state.h
+)
+
+include_directories(..)
+include_directories(../../include)
+
+set(COPYPROF_CFLAGS ${SANITIZER_COMMON_CFLAGS})
+set(COPYPROF_COMMON_DEFINITIONS "")
+append_rtti_flag(OFF COPYPROF_CFLAGS)
+
+add_compiler_rt_object_libraries(RTCopyProf
+  ARCHS ${COPYPROF_SUPPORTED_ARCH}
+  SOURCES ${COPYPROF_SOURCES}
+  ADDITIONAL_HEADERS ${COPYPROF_HEADERS}
+  CFLAGS ${COPYPROF_CFLAGS}
+  DEFS ${COPYPROF_COMMON_DEFINITIONS})
+
+# Build CopyProf runtimes shipped with Clang.
+add_compiler_rt_component(copyprof)
+
+set(COPYPROF_COMMON_RUNTIME_OBJECT_LIBS
+  RTInterception
+  RTSanitizerCommon
+  RTSanitizerCommonLibc
+  RTSanitizerCommonSymbolizer
+)
+
+add_compiler_rt_runtime(clang_rt.copyprof
+  STATIC
+  ARCHS ${COPYPROF_SUPPORTED_ARCH}
+  OBJECT_LIBS
+  RTCopyProf
+  ${COPYPROF_COMMON_RUNTIME_OBJECT_LIBS}
+  CFLAGS ${COPYPROF_CFLAGS}
+  DEFS ${COPYPROF_COMMON_DEFINITIONS}
+  PARENT_TARGET copyprof)
+
+if(SANITIZER_USE_SYMBOLS)
+  foreach(arch ${COPYPROF_SUPPORTED_ARCH})
+    add_sanitizer_rt_symbols(clang_rt.copyprof
+      ARCHS ${arch}
+      EXTRA copyprof.syms.extra)
+    add_dependencies(copyprof clang_rt.copyprof-${arch}-symbols)
+  endforeach()
+endif()
+
+if(COMPILER_RT_INCLUDE_TESTS)
+  add_subdirectory(tests)
+endif()
diff --git a/compiler-rt/lib/copyprof/copyprof.cpp b/compiler-rt/lib/copyprof/copyprof.cpp
new file mode 100644
index 00000000000000..854f9936847ce6
--- /dev/null
+++ b/compiler-rt/lib/copyprof/copyprof.cpp
@@ -0,0 +1,156 @@
+//===-- 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 core CopyProf runtime initialization and callback
+/// functions inserted by the instrumentation passes.
+///
+//===----------------------------------------------------------------------===//
+
+#include "copyprof_interface_internal.h"
+#include "copyprof_reporting.h"
+#include "copyprof_shadow.h"
+#include "copyprof_state.h"
+#include "sanitizer_common/sanitizer_common.h"
+#include "sanitizer_common/sanitizer_flags.h"
+#include "sanitizer_common/sanitizer_internal_defs.h"
+
+using namespace __copyprof;
+
+namespace __copyprof {
+
+// Whether CopyProf has been initialized.
+bool copyprof_is_initialized;
+// Whether CopyProf is currently initializing.
+bool copyprof_init_is_running;
+
+static void CheckUnwind() {
+  UNINITIALIZED BufferedStackTrace trace;
+  trace.Unwind(StackTrace::GetCurrentPc(), GET_CURRENT_FRAME(),
+               /*context=*/nullptr, common_flags()->fast_unwind_on_check);
+  trace.Print();
+}
+
+static void Initialize() {
+  if (LIKELY(copyprof_is_initialized))
+    return;
+  CHECK(!copyprof_init_is_running &&
+        "BUG: CopyProf Initialize() must not call itself.");
+  copyprof_init_is_running = true;
+  CacheBinaryName();
+  SetCheckUnwindCallback(&CheckUnwind);
+  InitializePlatformEarly();
+  SetCommonFlagsDefaults();
+  InitializeCommonFlags();
+  InitializeShadowMemory();
+  copyprof_init_is_running = false;
+  copyprof_is_initialized = true;
+}
+
+static void MaybeUpdateSmfContext(SmfContext context) {
+  // Only entering a top level special member function changes the current
+  // context. The context logically remains the same until control flow leaves
+  // the top level function.
+  if (__copyprof_state.smf_context == SmfContext::NONE ||
+      (__copyprof_state.construct_nesting_level == 0 &&
+       __copyprof_state.copy_nesting_level == 0 &&
+       __copyprof_state.destruct_nesting_level == 0)) {
+    __copyprof_state.smf_context = context;
+  }
+}
+
+// Updates shadow memory for `[addr, addr + size)` according to the current SMF
+// context: no update in `DTOR` context (objects may mutate their memory during
+// destruction, but that must not invalidate its classification as an
+// unnecessary copy), marked as copy in `COPY` context, and marked as non-copy
+// otherwise.
+static void UpdateShadow(const void* addr, uptr size) {
+  if (UNLIKELY(__copyprof_state.smf_context == SmfContext::DTOR))
+    return;
+  MarkApplicationMemory(addr, size,
+                        __copyprof_state.smf_context == SmfContext::COPY);
+}
+
+static void CopyMemberFunctionEnter(const void* this_ptr, uptr obj_size) {
+  MaybeUpdateSmfContext(SmfContext::COPY);
+  if (__copyprof_state.copy_nesting_level++ == 0) {
+    __copyprof_state.current_this_ptr = this_ptr;
+  }
+  UpdateShadow(this_ptr, obj_size);
+}
+
+static void CopyMemberFunctionExit(const void* this_ptr, uptr obj_size) {
+  --__copyprof_state.copy_nesting_level;
+  MaybeUpdateSmfContext(SmfContext::NONE);
+}
+
+}  // namespace __copyprof
+
+void __copyprof_init() { Initialize(); }
+
+void __copyprof_ctor_enter_callback(const void* this_ptr, uptr obj_size) {
+  MaybeUpdateSmfContext(SmfContext::CTOR);
+  ++__copyprof_state.construct_nesting_level;
+  UpdateShadow(this_ptr, obj_size);
+}
+
+void __copyprof_ctor_exit_callback(const void* this_ptr, uptr obj_size) {
+  --__copyprof_state.construct_nesting_level;
+  MaybeUpdateSmfContext(SmfContext::NONE);
+}
+
+void __copyprof_copy_ctor_enter_callback(const void* this_ptr,
+                                         const void* other_ptr, uptr obj_size) {
+  CopyMemberFunctionEnter(this_ptr, obj_size);
+}
+
+void __copyprof_copy_ctor_exit_callback(const void* this_ptr,
+                                        const void* other_ptr, uptr obj_size) {
+  CopyMemberFunctionExit(this_ptr, obj_size);
+}
+
+void __copyprof_copy_assign_op_enter_callback(const void* this_ptr,
+                                              const void* other_ptr,
+                                              uptr obj_size) {
+  CopyMemberFunctionEnter(this_ptr, obj_size);
+}
+
+void __copyprof_copy_assign_op_exit_callback(const void* this_ptr,
+                                             const void* other_ptr,
+                                             uptr obj_size) {
+  CopyMemberFunctionExit(this_ptr, obj_size);
+}
+
+void __copyprof_dtor_enter_callback(const void* this_ptr, uptr obj_size) {
+  MaybeUpdateSmfContext(SmfContext::DTOR);
+  if (__copyprof_state.destruct_nesting_level++ == 0) {
+    // Control flow just entered the top-level d'tor. Optimistically mark
+    // `is_transitive_copy` as `true`. If any subsequent d'tor observes object
+    // memory marked as not copy, then the flag will be set to `false`.
+    __copyprof_state.is_transitive_copy = true;
+  }
+}
+
+void __copyprof_dtor_exit_callback(const void* this_ptr, uptr obj_size) {
+  --__copyprof_state.destruct_nesting_level;
+  MaybeUpdateSmfContext(SmfContext::NONE);
+  // If this is the top level-dtor and `this` is transitively marked as copy,
+  // then an object has been found whose transitively owned memory is marked as
+  // copy, so a report is logged.
+  __copyprof_state.is_transitive_copy &= IsMarkedAsCopy(this_ptr, obj_size);
+  if (__copyprof_state.destruct_nesting_level == 0 &&
+      __copyprof_state.is_transitive_copy) {
+    // TODO: Dynamic allocation tracking via malloc/new interception will be
+    // added in a subsequent patch. For now, did_allocate is set to true.
+    LogCopyProfReport(GET_CALLER_PC(), GET_CURRENT_FRAME(), obj_size,
+                      /*did_allocate=*/true);
+  }
+}
+
+void __copyprof_store_callback(const void* addr, uptr size) {
+  UpdateShadow(addr, size);
+}
diff --git a/compiler-rt/lib/copyprof/copyprof.syms.extra b/compiler-rt/lib/copyprof/copyprof.syms.extra
new file mode 100644
index 00000000000000..6d640e1fa23843
--- /dev/null
+++ b/compiler-rt/lib/copyprof/copyprof.syms.extra
@@ -0,0 +1 @@
+__copyprof_*
diff --git a/compiler-rt/lib/copyprof/copyprof_interface_internal.h b/compiler-rt/lib/copyprof/copyprof_interface_internal.h
new file mode 100644
index 00000000000000..58e6f84e442bb1
--- /dev/null
+++ b/compiler-rt/lib/copyprof/copyprof_interface_internal.h
@@ -0,0 +1,50 @@
+//===-- copyprof_interface_internal.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 internal runtime interface for CopyProf, including
+/// initialization and special member function callback declarations.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef COPYPROF_INTERFACE_INTERNAL_H
+#define COPYPROF_INTERFACE_INTERNAL_H
+
+#include "copyprof_internal.h"
+#include "sanitizer_common/sanitizer_internal_defs.h"
+
+extern "C" {
+
+// Should be called at the very beginning of the process before any instrumented
+// code executes.
+SANITIZER_INTERFACE_ATTRIBUTE void __copyprof_init();
+
+// Runtime callbacks that update the CopyProf state machine and shadow memory
+// when entering or leaving special member functions.
+SANITIZER_INTERFACE_ATTRIBUTE void __copyprof_ctor_enter_callback(
+    const void* this_ptr, uptr obj_size);
+SANITIZER_INTERFACE_ATTRIBUTE void __copyprof_ctor_exit_callback(
+    const void* this_ptr, uptr obj_size);
+SANITIZER_INTERFACE_ATTRIBUTE void __copyprof_copy_ctor_enter_callback(
+    const void* this_ptr, const void* other_ptr, uptr obj_size);
+SANITIZER_INTERFACE_ATTRIBUTE void __copyprof_copy_ctor_exit_callback(
+    const void* this_ptr, const void* other_ptr, uptr obj_size);
+SANITIZER_INTERFACE_ATTRIBUTE void __copyprof_copy_assign_op_enter_callback(
+    const void* this_ptr, const void* other_ptr, uptr obj_size);
+SANITIZER_INTERFACE_ATTRIBUTE void __copyprof_copy_assign_op_exit_callback(
+    const void* this_ptr, const void* other_ptr, uptr obj_size);
+SANITIZER_INTERFACE_ATTRIBUTE void __copyprof_dtor_enter_callback(
+    const void* this_ptr, uptr obj_size);
+SANITIZER_INTERFACE_ATTRIBUTE void __copyprof_dtor_exit_callback(
+    const void* this_ptr, uptr obj_size);
+// Store instructions callback that marks memory as not copy.
+SANITIZER_INTERFACE_ATTRIBUTE void __copyprof_store_callback(const void* addr,
+                                                             uptr size);
+
+}  // extern "C"
+
+#endif  // COPYPROF_INTERFACE_INTERNAL_H
diff --git a/compiler-rt/lib/copyprof/copyprof_internal.h b/compiler-rt/lib/copyprof/copyprof_internal.h
new file mode 100644
index 00000000000000..ccd9c95fe4dd43
--- /dev/null
+++ b/compiler-rt/lib/copyprof/copyprof_internal.h
@@ -0,0 +1,33 @@
+//===-- copyprof_internal.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 internal runtime data structures, flags, and helper
+/// functions shared within the CopyProf runtime library.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef COPYPROF_INTERNAL_H
+#define COPYPROF_INTERNAL_H
+
+#include "sanitizer_common/sanitizer_internal_defs.h"
+#include "sanitizer_common/sanitizer_stacktrace.h"
+
+using __sanitizer::u32;
+using __sanitizer::u64;
+using __sanitizer::uptr;
+using __sanitizer::usize;
+
+namespace __copyprof {
+
+using __sanitizer::BufferedStackTrace;
+extern bool copyprof_is_initialized;
+extern bool copyprof_init_is_running;
+
+}  // namespace __copyprof
+
+#endif  // COPYPROF_INTERNAL_H
diff --git a/compiler-rt/lib/copyprof/copyprof_reporting.cpp b/compiler-rt/lib/copyprof/copyprof_reporting.cpp
new file mode 100644
index 00000000000000..439438bc12c842
--- /dev/null
+++ b/compiler-rt/lib/copyprof/copyprof_reporting.cpp
@@ -0,0 +1,42 @@
+//===-- copyprof_reporting.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 reporting mechanisms for CopyProf, generating
+/// reports with stack traces when unnecessary object copies are destroyed.
+///
+//===----------------------------------------------------------------------===//
+
+#include "copyprof_reporting.h"
+
+#include "copyprof_internal.h"
+#include "sanitizer_common/sanitizer_common.h"
+#include "sanitizer_common/sanitizer_stacktrace.h"
+
+namespace __copyprof {
+namespace {
+
+// TODO: Make configurable via flags.
+constexpr int kMaxNumStackFrames = 30;
+
+}  // namespace
+
+void LogCopyProfReport(uptr pc, uptr bp, uptr obj_size, bool did_allocate) {
+  // FIXME: replace hard coded object size with flag.
+  if (obj_size <= 16 || !did_allocate)
+    return;
+  UNINITIALIZED BufferedStackTrace stack_trace;
+  stack_trace.Unwind(pc, bp, /*context=*/nullptr,
+                     common_flags()->fast_unwind_on_fatal, kMaxNumStackFrames);
+  InternalScopedString output;
+  output.AppendF(
+      "[copyprof] Destroyed unnecessary copy amounting to %zu bytes:\n",
+      (usize)obj_size);
+  stack_trace.PrintTo(&output);
+  Printf("%s", output.data());
+}
+}  // namespace __copyprof
diff --git a/compiler-rt/lib/copyprof/copyprof_reporting.h b/compiler-rt/lib/copyprof/copyprof_reporting.h
new file mode 100644
index 00000000000000..17b5be426346fd
--- /dev/null
+++ b/compiler-rt/lib/copyprof/copyprof_reporting.h
@@ -0,0 +1,28 @@
+//===-- copyprof_reporting.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 reporting interface that is used to log unnecessary
+// copies identified during object destruction.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef COPYPROF_REPORTING_H_
+#define COPYPROF_REPORTING_H_
+
+#include "sanitizer_common/sanitizer_common.h"
+
+namespace __copyprof {
+
+// Prints a CopyProf report to stderr. `pc` and `bp` are the program counter
+// and frame pointer where the copy was destroyed, `obj_size` its flat size in
+// bytes, and `did_allocate` whether the copy allocated memory.
+void LogCopyProfReport(uptr pc, uptr bp, uptr obj_size, bool did_allocate);
+
+}  // namespace __copyprof
+
+#endif  // COPYPROF_REPORTING_H_
diff --git a/compiler-rt/lib/copyprof/copyprof_shadow.cpp b/compiler-rt/lib/copyprof/copyprof_shadow.cpp
new file mode 100644
index 00000000000000..e5b52baa8acd88
--- /dev/null
+++ b/compiler-rt/lib/copyprof/copyprof_shadow.cpp
@@ -0,0 +1,112 @@
+//===-- copyprof_shadow.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 shadow memory management for CopyProf, tracking
+/// the copy status and modification state of application memory.
+///
+//===----------------------------------------------------------------------===//
+
+#include "copyprof_shadow.h"
+
+#include "sanitizer_common/sanitizer_internal_defs.h"
+
+namespace __copyprof {
+namespace {
+
+constexpr uptr kBitsPerShadowByte = 8;
+static_assert(
+    1 << kShadowScale == kBitsPerShadowByte,
+    "CopyProf tracks 1 bit per application byte (8 bits per shadow byte)");
+
+// Tracks whether application memory is marked as a copy.
+ShadowMemory g_copy_shadow;
+
+// Returns a mask with bits [`start_bit`, `end_bit`) set.
+unsigned char BitMask(uptr start_bit, uptr end_bit) {
+  CHECK_LE(start_bit, end_bit);
+  return static_cast<unsigned char>((1 << end_bit) - (1 << start_bit));
+}
+
+// Whether bits [`start_bit`, `end_bit`) of `shadow_byte` are all set.
+bool AllBitsSet(unsigned char shadow_byte, uptr start_bit, uptr end_bit) {
+  const unsigned char mask = BitMask(start_bit, end_bit);
+  return (shadow_byte & mask) == mask;
+}
+
+// Sets (or clears) bits [`start_bit`, `end_bit`) of `*shadow_byte`.
+void SetBits(unsigned char* shadow_byte, uptr start_bit, uptr end_bit,
+             bool is_copy) {
+  const unsigned char mask = BitMask(start_bit, end_bit);
+  if (is_copy)
+    *shadow_byte |= mask;
+  else
+    *shadow_byte &= static_cast<unsigned char>(~mask);
+}
+
+uptr BytesToShadowBits(uptr num_bytes) {
+  // Each application byte maps to one shadow bit.
+  return num_bytes;
+}
+
+}  // namespace
+
+void InitializeShadowMemory() {
+  g_copy_shadow = ShadowMemory::Create("copyprof");
+}
+
+void MarkApplicationMemory(const void* app_addr, uptr num_bytes, bool is_copy) {
+  CHECK_GT(num_bytes, 0);
+  const uptr addr = reinterpret_cast<uptr>(app_addr);
+  unsigned char* shadow =
+      reinterpret_cast<unsigned char*>(g_copy_shadow.MemToShadow(addr));
+
+  // An application range need not start on a shadow byte boundary, so it is
+  // updated in three steps: the leading (possibly partial) shadow byte, the
+  // whole shadow bytes in the middle, and the trailing partial byte.
+  uptr num_shadow_bits = BytesToShadowBits(num_bytes);
+  if (uptr start_bit = addr % kBitsPerShadowByte; start_bit > 0) {
+    uptr end_bit =
+        start_bit + Min(kBitsPerShadowByte - start_bit, num_shadow_bits);
+    SetBits(shadow++, start_bit, end_bit, is_copy);
+    num_shadow_bits -= end_bit - start_bit;
+  }
+  if (uptr full_bytes = num_shadow_bits / kBitsPerShadowByte; full_bytes > 0) {
+    internal_memset(shadow, is_copy ? 0xFF : 0, full_bytes);
+    shadow += full_bytes;
+    num_shadow_bits -= full_bytes * kBitsPerShadowByte;
+  }
+  if (num_shadow_bits > 0)
+    SetBits(shadow, /*start_bit=*/0, num_shadow_bits, is_copy);
+}
+
+bool IsMarkedAsCopy(const void* app_addr, uptr num_bytes) {
+  CHECK_GT(num_bytes, 0);
+  const uptr addr = reinterpret_cast<uptr>(app_addr);
+  const auto* shadow =
+      reinterpret_cast<const unsigned char*>(g_copy_shadow.MemToShadow(addr));
+
+  uptr num_shadow_bits = BytesToShadowBits(num_bytes);
+  if (uptr start_bit = addr % kBitsPerShadowByte; start_bit > 0) {
+    uptr end_bit =
+        start_bit + Min(kBitsPerShadowByte - start_bit, num_shadow_bits);
+    if (!AllBitsSet(*shadow++, start_bit, end_bit))
+      return false;
+    num_shadow_bits -= end_bit - start_bit;
+  }
+  uptr full_bytes = num_shadow_bits / kBitsPerShadowByte;
+  for (uptr i = 0; i < full_bytes; ++i) {
+    if (shadow[i] != 0xFF)
+      return false;
+  }
+  shadow += full_bytes;
+  num_shadow_bits -= full_bytes * kBitsPerShadowByte;
+  return num_shadow_bits == 0 ||
+         AllBitsSet(*shadow, /*start_bit=*/0, num_shadow_bits);
+}
+
+}  // namespace __copyprof
diff --git a/compiler-rt/lib/copyprof/copyprof_shadow.h b/compiler-rt/lib/copyprof/copyprof_shadow.h
new file mode 100644
index 00000000000000..0a7334dd2d1646
--- /dev/null
+++ b/compiler-rt/lib/copyprof/copyprof_shadow.h
@@ -0,0 +1,63 @@
+//===-- copyprof_shadow.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 shadow memory interface and template helpers for
+/// mapping application memory to CopyProf shadow memory.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef COPYPROF_SHADOW_H
+#define COPYPROF_SHADOW_H
+
+#include "sanitizer_common/sanitizer_common.h"
+#include "sanitizer_common/sanitizer_internal_defs.h"
+
+namespace __copyprof {
+
+// FIXME: copyprof uses a 1:8 mapping but this may lead to data races on shadow
+// memory for concurrent stores within the same 8 byte region. Consider using a
+// 1:1 mapping or reducing granularity (e.g. atomically store whole bytes for
+// each update to an 8 byte region).
+constexpr uptr kShadowScale = 3;
+
+// Helper for mapping application addresses to shadow memory.
+struct ShadowMemory {
+  static uptr MemToShadowSize(uptr size) { return size >> kShadowScale; }
+  static ShadowMemory Create(const char* name) {
+    uptr max_user_va = GetMaxUserVirtualAddress();
+    uptr shadow_size_bytes =
+        RoundUpTo(MemToShadowSize(max_user_va), GetMmapGranularity());
+    uptr mapped = MapDynamicShadow(shadow_size_bytes, kShadowScale,
+                                   /*min_shadow_base_alignment=*/0, max_user_va,
+                                   GetMmapGranularity());
+    ReserveShadowMemoryRange(mapped, mapped + shadow_size_bytes - 1, name,
+                             /*madvise_shadow=*/true);
+    return ShadowMemory(mapped);
+  }
+  ShadowMemory() = default;
+  uptr MemToShadow(uptr p) const { return (p >> kShadowScale) + shadow_base_; }
+
+ private:
+  explicit ShadowMemory(uptr shadow_base) : shadow_base_(shadow_base) {}
+  uptr shadow_base_ = 0;
+};
+
+// Must be called exactly once at program startup.
+void InitializeShadowMemory();
+
+// Given an application memory block starting at `app_addr` of size `num_bytes`,
+// marks the corresponding shadow memory as a copy or non-copy.
+void MarkApplicationMemory(const void* app_addr, uptr num_bytes, bool is_copy);
+
+// Whether the application memory block starting at `app_addr` of size
+// `num_bytes` is marked as a copy in shadow memory.
+bool IsMarkedAsCopy(const void* app_addr, uptr num_bytes);
+
+}  // namespace __copyprof
+
+#endif  // COPYPROF_SHADOW_H
diff --git a/compiler-rt/lib/copyprof/copyprof_stack.cpp b/compiler-rt/lib/copyprof/copyprof_stack.cpp
new file mode 100644
index 00000000000000..440a0986938c08
--- /dev/null
+++ b/compiler-rt/lib/copyprof/copyprof_stack.cpp
@@ -0,0 +1,24 @@
+//===-- copyprof_stack.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 stack unwinding utilities for CopyProf, enabling
+/// stack trace collection for copy profiling reports.
+///
+//===----------------------------------------------------------------------===//
+
+#include "sanitizer_common/sanitizer_stacktrace.h"
+
+namespace __sanitizer {
+
+void BufferedStackTrace::UnwindImpl(uptr pc, uptr bp, void* context,
+                                    bool request_fast, u32 max_depth) {
+  Unwind(max_depth, pc, bp, context, 0, 0,
+         StackTrace::WillUseFastUnwind(request_fast));
+}
+
+}  // namespace __sanitizer
diff --git a/compiler-rt/lib/copyprof/copyprof_state.cpp b/compiler-rt/lib/copyprof/copyprof_state.cpp
new file mode 100644
index 00000000000000..4da7b1f77ff1a8
--- /dev/null
+++ b/compiler-rt/lib/copyprof/copyprof_state.cpp
@@ -0,0 +1,20 @@
+//===-- copyprof_state.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 per-thread and per-object state management for
+/// tracking execution context within special member functions.
+///
+//===----------------------------------------------------------------------===//
+
+#include "copyprof_state.h"
+
+namespace __copyprof {
+
+THREADLOCAL PerThreadState __copyprof_state;
+
+}  // namespace __copyprof
diff --git a/compiler-rt/lib/copyprof/copyprof_state.h b/compiler-rt/lib/copyprof/copyprof_state.h
new file mode 100644
index 00000000000000..c9b2717aa1d254
--- /dev/null
+++ b/compiler-rt/lib/copyprof/copyprof_state.h
@@ -0,0 +1,70 @@
+//===-- copyprof_state.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 per-thread and per-object state structures used to
+/// track special member function nesting and dynamic memory allocations.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef COPYPROF_STATE_H
+#define COPYPROF_STATE_H
+
+#include "sanitizer_common/sanitizer_internal_defs.h"
+
+namespace __copyprof {
+
+// Determines the special member function (SMF) execution context of a thread,
+// dictating how stores and allocations affect shadow memory.
+// The first time control flow reaches the entry point of a special member
+// function, the current SMF context is changed accordingly.
+// A copy c'tor or copy assignment operator sets the context to `COPY`, a c'tor
+// to `CTOR`, and a d'tor to `DTOR`.
+// In the `COPY` context, shadow memory is marked as copy.
+// In the `CTOR` context, shadow memory is marked as non-copy.
+// In the `DTOR` context, no shadow memory is updated at all (to
+// avoid false negatives during destruction). Once control flow leaves (any
+// nested) special member functions, the context is set to `NONE`. In this
+// state, any stores mark shadow memory as non-copy.
+enum class SmfContext : u8 {
+  NONE,
+  CTOR,
+  COPY,
+  DTOR,
+};
+
+// CopyProf uses per-thread state to figure out whether control flow is
+// currently inside a special member function, and adapts updating of shadow
+// memory accordingly (see SmfContext). Since special member functions can
+// nest arbitrarily, this state needs to be kept across function calls, so this
+// state is stored in TLS. The nesting level counters are used to determine
+// whether a top-level (i.e. the first in the call stack of special member
+// functions) special member function has been reached.
+struct PerThreadState {
+  u32 construct_nesting_level = 0;
+  u32 copy_nesting_level = 0;
+  u32 destruct_nesting_level = 0;
+  SmfContext smf_context = SmfContext::NONE;
+  // Whether all transitively reachable d'tors from the top-level d'tor have
+  // observed copies.
+  bool is_transitive_copy = false;
+  // When control flow enters a special member function, this is set to the
+  // `this` pointer of the current object. This is used to look up the
+  // per-object state outside of special member functions (e.g. when allocating
+  // memory).
+  const void* current_this_ptr = nullptr;
+};
+
+// The runtime is always linked into the main executable, so the state can be
+// reached with the initial-exec model instead of paying for a __tls_get_addr
+// call on every access.
+__attribute__((tls_model("initial-exec")))
+extern THREADLOCAL PerThreadState __copyprof_state;
+
+}  // namespace __copyprof
+
+#endif  // COPYPROF_STATE_H
diff --git a/compiler-rt/lib/copyprof/tests/CMakeLists.txt b/compiler-rt/lib/copyprof/tests/CMakeLists.txt
new file mode 100644
index 00000000000000..948d7a1cb88a7b
--- /dev/null
+++ b/compiler-rt/lib/copyprof/tests/CMakeLists.txt
@@ -0,0 +1,73 @@
+include(CheckCXXCompilerFlag)
+include(CompilerRTCompile)
+
+set(COPYPROF_UNITTEST_NOINST_CFLAGS
+  ${COMPILER_RT_UNITTEST_CFLAGS}
+  ${COMPILER_RT_GTEST_CFLAGS}
+  ${COMPILER_RT_GMOCK_CFLAGS}
+  ${SANITIZER_TEST_CXX_CFLAGS}
+  -I${COMPILER_RT_SOURCE_DIR}/include
+  -I${COMPILER_RT_SOURCE_DIR}/lib
+  -DSANITIZER_COMMON_NO_REDEFINE_BUILTINS
+  -O1
+  -g
+  -Wno-pedantic
+  -fno-omit-frame-pointer
+  -fno-rtti)
+
+file(GLOB COPYPROF_HEADERS ../*.h)
+
+set(COPYPROF_NOINST_TEST_SOURCES
+  driver.cpp
+  copyprof_shadow_test.cpp
+  copyprof_state_test.cpp)
+
+include_directories(.. ../..)
+
+set(COPYPROF_UNIT_TEST_HEADERS
+  ${COPYPROF_HEADERS})
+
+set(COPYPROF_UNITTEST_LINK_FLAGS
+  ${COMPILER_RT_UNITTEST_LINK_FLAGS})
+list(APPEND COPYPROF_UNITTEST_LINK_FLAGS -pthread)
+list(APPEND COPYPROF_UNITTEST_LINK_FLAGS ${SANITIZER_TEST_CXX_LIBRARIES})
+append_list_if(COMPILER_RT_HAS_LIBDL -ldl COPYPROF_UNITTEST_LINK_FLAGS)
+append_list_if(COMPILER_RT_HAS_LIBRT -lrt COPYPROF_UNITTEST_LINK_FLAGS)
+append_list_if(COMPILER_RT_HAS_LIBM -lm COPYPROF_UNITTEST_LINK_FLAGS)
+
+# Adds CopyProf tests for each architecture.
+macro(add_copyprof_tests_for_arch arch)
+  set(COPYPROF_TEST_RUNTIME_OBJECTS
+    $<TARGET_OBJECTS:RTCopyProf.${arch}>
+    $<TARGET_OBJECTS:RTInterception.${arch}>
+    $<TARGET_OBJECTS:RTSanitizerCommon.${arch}>
+    $<TARGET_OBJECTS:RTSanitizerCommonLibc.${arch}>
+    $<TARGET_OBJECTS:RTSanitizerCommonCoverage.${arch}>
+    $<TARGET_OBJECTS:RTSanitizerCommonSymbolizer.${arch}>
+    $<TARGET_OBJECTS:RTSanitizerCommonSymbolizerInternal.${arch}>
+  )
+  set(COPYPROF_TEST_RUNTIME RTCopyProfTest.${arch})
+  add_library(${COPYPROF_TEST_RUNTIME} STATIC ${COPYPROF_TEST_RUNTIME_OBJECTS})
+  set_target_properties(${COPYPROF_TEST_RUNTIME} PROPERTIES
+    ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+    FOLDER "Compiler-RT/Tests/Runtime"
+  )
+  generate_compiler_rt_tests(COPYPROF_NOINST_TEST_OBJECTS
+    CopyProfUnitTests "CopyProf-${arch}-UnitTest" ${arch}
+    RUNTIME ${COPYPROF_TEST_RUNTIME}
+    SOURCES ${COPYPROF_NOINST_TEST_SOURCES} ${COMPILER_RT_GTEST_SOURCE} ${COMPILER_RT_GMOCK_SOURCE}
+    COMPILE_DEPS ${COPYPROF_UNIT_TEST_HEADERS}
+    CFLAGS ${COPYPROF_UNITTEST_NOINST_CFLAGS}
+    LINK_FLAGS ${COPYPROF_UNITTEST_LINK_FLAGS})
+endmacro()
+
+# CopyProf unit tests testsuite.
+add_custom_target(CopyProfUnitTests)
+set_target_properties(CopyProfUnitTests PROPERTIES FOLDER "Compiler-RT/Tests")
+
+if(COMPILER_RT_CAN_EXECUTE_TESTS AND COMPILER_RT_DEFAULT_TARGET_ARCH IN_LIST COPYPROF_SUPPORTED_ARCH)
+  # CopyProf unit tests are only run on the host machine.
+  foreach(arch ${COMPILER_RT_DEFAULT_TARGET_ARCH})
+    add_copyprof_tests_for_arch(${arch})
+  endforeach()
+endif()
diff --git a/compiler-rt/lib/copyprof/tests/copyprof_shadow_test.cpp b/compiler-rt/lib/copyprof/tests/copyprof_shadow_test.cpp
new file mode 100644
index 00000000000000..cdb209ad60d79e
--- /dev/null
+++ b/compiler-rt/lib/copyprof/tests/copyprof_shadow_test.cpp
@@ -0,0 +1,164 @@
+//===-- copyprof_shadow_test.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
+//
+//===----------------------------------------------------------------------===//
+
+#include "copyprof/copyprof_shadow.h"
+
+#include "copyprof/copyprof_interface_internal.h"
+#include "gtest/gtest.h"
+
+namespace __copyprof {
+namespace {
+
+TEST(CopyProfShadowTest, AlignedMemory) {
+  __copyprof_init();
+  u64 buf[4] = {0};
+  MarkApplicationMemory(buf, sizeof(buf), /*is_copy=*/true);
+  EXPECT_TRUE(IsMarkedAsCopy(buf, sizeof(buf)));
+  MarkApplicationMemory(buf, sizeof(buf), /*is_copy=*/false);
+  EXPECT_FALSE(IsMarkedAsCopy(buf, sizeof(buf)));
+}
+
+TEST(CopyProfShadowTest, UnalignedMemory) {
+  __copyprof_init();
+  alignas(8) unsigned char buf[64] = {0};
+  MarkApplicationMemory(buf, sizeof(buf), /*is_copy=*/false);
+
+  // Mark an unaligned slice in the middle as a copy.
+  MarkApplicationMemory(buf + 3, 5, /*is_copy=*/true);
+  EXPECT_TRUE(IsMarkedAsCopy(buf + 3, 5));
+
+  // Ensure surrounding unassigned bytes remain marked as non-copy.
+  EXPECT_FALSE(IsMarkedAsCopy(buf, 3));
+  EXPECT_FALSE(IsMarkedAsCopy(buf + 8, 8));
+}
+
+// An unaligned range that spans more than one shadow byte. Updating whole
+// shadow bytes without accounting for the start bit corrupts the bits of the
+// object sharing the leading shadow byte, and never marks the bits past it.
+// Bytes are queried one at a time on purpose: a range query would share any
+// masking bug with the update path and hide the defect.
+TEST(CopyProfShadowTest, UnalignedRangeSpanningShadowBytes) {
+  __copyprof_init();
+  alignas(8) unsigned char buf[32] = {0};
+  MarkApplicationMemory(buf, sizeof(buf), /*is_copy=*/false);
+
+  // Eight bytes at offset 4: bits 4-7 of the first shadow byte and bits 0-3 of
+  // the second.
+  MarkApplicationMemory(buf + 4, 8, /*is_copy=*/true);
+
+  for (uptr i = 0; i < 4; ++i)
+    EXPECT_FALSE(IsMarkedAsCopy(buf + i, 1)) << "byte " << i;
+  for (uptr i = 4; i < 12; ++i)
+    EXPECT_TRUE(IsMarkedAsCopy(buf + i, 1)) << "byte " << i;
+  for (uptr i = 12; i < 16; ++i)
+    EXPECT_FALSE(IsMarkedAsCopy(buf + i, 1)) << "byte " << i;
+}
+
+// A range whose bits straddle a shadow byte boundary, i.e. where
+// `start_bit + num_bits` exceeds the bits in one shadow byte. The overflowing
+// bits belong to the next shadow byte and must not be truncated away.
+TEST(CopyProfShadowTest, UnalignedRangeCrossingByteBoundary) {
+  __copyprof_init();
+  alignas(8) unsigned char buf[32] = {0};
+  MarkApplicationMemory(buf, sizeof(buf), /*is_copy=*/false);
+
+  // Four bytes at offset 6: bits 6-7 of the first shadow byte and bits 0-1 of
+  // the second.
+  MarkApplicationMemory(buf + 6, 4, /*is_copy=*/true);
+
+  for (uptr i = 0; i < 6; ++i)
+    EXPECT_FALSE(IsMarkedAsCopy(buf + i, 1)) << "byte " << i;
+  for (uptr i = 6; i < 10; ++i)
+    EXPECT_TRUE(IsMarkedAsCopy(buf + i, 1)) << "byte " << i;
+  for (uptr i = 10; i < 16; ++i)
+    EXPECT_FALSE(IsMarkedAsCopy(buf + i, 1)) << "byte " << i;
+}
+
+// The same geometry in the clearing direction: a store to an unaligned field
+// must not clear the copy bits of the bytes around it.
+TEST(CopyProfShadowTest, ClearingUnalignedRangeKeepsNeighbours) {
+  __copyprof_init();
+  alignas(8) unsigned char buf[32] = {0};
+  MarkApplicationMemory(buf, sizeof(buf), /*is_copy=*/true);
+
+  MarkApplicationMemory(buf + 6, 4, /*is_copy=*/false);
+
+  for (uptr i = 0; i < 6; ++i)
+    EXPECT_TRUE(IsMarkedAsCopy(buf + i, 1)) << "byte " << i;
+  for (uptr i = 6; i < 10; ++i)
+    EXPECT_FALSE(IsMarkedAsCopy(buf + i, 1)) << "byte " << i;
+  for (uptr i = 10; i < 16; ++i)
+    EXPECT_TRUE(IsMarkedAsCopy(buf + i, 1)) << "byte " << i;
+}
+
+// A range that starts and ends inside the same shadow byte, touching neither of
+// its boundaries. Only the bits of the range itself may change.
+TEST(CopyProfShadowTest, UnalignedRangeWithinShadowByte) {
+  __copyprof_init();
+  alignas(8) unsigned char buf[16] = {0};
+  MarkApplicationMemory(buf, sizeof(buf), /*is_copy=*/false);
+
+  // Three bytes at offset 2: bits 2-4 of the first shadow byte.
+  MarkApplicationMemory(buf + 2, 3, /*is_copy=*/true);
+
+  for (uptr i = 0; i < 2; ++i)
+    EXPECT_FALSE(IsMarkedAsCopy(buf + i, 1)) << "byte " << i;
+  for (uptr i = 2; i < 5; ++i)
+    EXPECT_TRUE(IsMarkedAsCopy(buf + i, 1)) << "byte " << i;
+  for (uptr i = 5; i < 16; ++i)
+    EXPECT_FALSE(IsMarkedAsCopy(buf + i, 1)) << "byte " << i;
+  EXPECT_TRUE(IsMarkedAsCopy(buf + 2, 3));
+}
+
+// A range that exercises all three steps in a single call: a leading partial
+// shadow byte, whole shadow bytes, and a trailing partial shadow byte.
+TEST(CopyProfShadowTest, UnalignedRangeSpanningWholeShadowBytes) {
+  __copyprof_init();
+  alignas(8) unsigned char buf[40] = {0};
+  MarkApplicationMemory(buf, sizeof(buf), /*is_copy=*/false);
+
+  // 22 bytes at offset 4: bits 4-7 of the first shadow byte, all bits of the
+  // second and third, and bits 0-1 of the fourth.
+  MarkApplicationMemory(buf + 4, 22, /*is_copy=*/true);
+
+  for (uptr i = 0; i < 4; ++i)
+    EXPECT_FALSE(IsMarkedAsCopy(buf + i, 1)) << "byte " << i;
+  for (uptr i = 4; i < 26; ++i)
+    EXPECT_TRUE(IsMarkedAsCopy(buf + i, 1)) << "byte " << i;
+  for (uptr i = 26; i < 40; ++i)
+    EXPECT_FALSE(IsMarkedAsCopy(buf + i, 1)) << "byte " << i;
+
+  // Range queries over the same geometry. Extending the range by one byte at
+  // either end reaches an unmarked byte in the leading or trailing step.
+  EXPECT_TRUE(IsMarkedAsCopy(buf + 4, 22));
+  EXPECT_FALSE(IsMarkedAsCopy(buf + 3, 23));
+  EXPECT_FALSE(IsMarkedAsCopy(buf + 4, 23));
+
+  // An unmarked byte within a whole shadow byte fails the range query.
+  MarkApplicationMemory(buf + 13, 1, /*is_copy=*/false);
+  EXPECT_FALSE(IsMarkedAsCopy(buf + 4, 22));
+}
+
+TEST(CopyProfShadowTest, PartialOverwrite) {
+  __copyprof_init();
+  u64 buf[4] = {0};
+  MarkApplicationMemory(buf, sizeof(buf), /*is_copy=*/true);
+  EXPECT_TRUE(IsMarkedAsCopy(buf, sizeof(buf)));
+
+  // Simulate modifying a sub-object or field in the middle of the buffer.
+  MarkApplicationMemory(&buf[1], sizeof(u64), /*is_copy=*/false);
+  EXPECT_FALSE(IsMarkedAsCopy(buf, sizeof(buf)));
+
+  // Untouched surrounding memory should still be marked as copy.
+  EXPECT_TRUE(IsMarkedAsCopy(&buf[0], sizeof(u64)));
+  EXPECT_TRUE(IsMarkedAsCopy(&buf[2], 2 * sizeof(u64)));
+}
+
+}  // namespace
+}  // namespace __copyprof
diff --git a/compiler-rt/lib/copyprof/tests/copyprof_state_test.cpp b/compiler-rt/lib/copyprof/tests/copyprof_state_test.cpp
new file mode 100644
index 00000000000000..a8fa15a4ebc708
--- /dev/null
+++ b/compiler-rt/lib/copyprof/tests/copyprof_state_test.cpp
@@ -0,0 +1,178 @@
+//===-- copyprof_state_test.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
+//
+//===----------------------------------------------------------------------===//
+
+#include "copyprof/copyprof_state.h"
+
+#include "copyprof/copyprof_interface_internal.h"
+#include "copyprof/copyprof_shadow.h"
+#include "gtest/gtest.h"
+
+namespace __copyprof {
+namespace {
+
+// The runtime keeps its per-thread state in a single object that the callbacks
+// mutate in place, so without a reset each test would inherit whatever nesting
+// levels, mode and flags the previous one left behind -- and a test that
+// aborts mid-scenario would cascade into the rest of the suite.
+//
+// This does NOT reset shadow memory: it is process-global and far too large to
+// clear between tests, and the buffers under test are stack addresses that get
+// reused across cases. Tests that depend on the shadow state of a buffer must
+// establish it explicitly with `MarkApplicationMemory`.
+class CopyProfStateTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    __copyprof_init();
+    __copyprof_state = PerThreadState();
+  }
+};
+
+TEST_F(CopyProfStateTest, NestingCounters) {
+  EXPECT_EQ(__copyprof_state.construct_nesting_level, 0u);
+  EXPECT_EQ(__copyprof_state.copy_nesting_level, 0u);
+  EXPECT_EQ(__copyprof_state.destruct_nesting_level, 0u);
+  EXPECT_EQ(__copyprof_state.smf_context, SmfContext::NONE);
+
+  // Enter constructor callback.
+  __copyprof_ctor_enter_callback(nullptr, 16);
+  EXPECT_EQ(__copyprof_state.construct_nesting_level, 1u);
+  EXPECT_EQ(__copyprof_state.smf_context, SmfContext::CTOR);
+
+  // Exit constructor callback.
+  __copyprof_ctor_exit_callback(nullptr, 16);
+  EXPECT_EQ(__copyprof_state.construct_nesting_level, 0u);
+  EXPECT_EQ(__copyprof_state.smf_context, SmfContext::NONE);
+}
+
+TEST_F(CopyProfStateTest, SimulatedUnnecessaryCopy) {
+  char obj[32] = {0};
+  char other[32] = {0};
+
+  // Simulate copy constructor execution.
+  __copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
+  EXPECT_EQ(__copyprof_state.copy_nesting_level, 1u);
+  EXPECT_EQ(__copyprof_state.smf_context, SmfContext::COPY);
+  __copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
+
+  EXPECT_TRUE(IsMarkedAsCopy(obj, sizeof(obj)));
+  EXPECT_EQ(__copyprof_state.smf_context, SmfContext::NONE);
+
+  // Simulate destructor without any intervening store callbacks.
+  __copyprof_dtor_enter_callback(obj, sizeof(obj));
+  EXPECT_EQ(__copyprof_state.smf_context, SmfContext::DTOR);
+  EXPECT_TRUE(__copyprof_state.is_transitive_copy);
+  __copyprof_dtor_exit_callback(obj, sizeof(obj));
+}
+
+TEST_F(CopyProfStateTest, ModifiedCopyIsNotMakedAsCopyAnymore) {
+  char obj[32] = {0};
+  char other[32] = {0};
+
+  // Create copy `obj` based on `other`.
+  __copyprof_copy_assign_op_enter_callback(obj, other, sizeof(obj));
+  __copyprof_copy_assign_op_exit_callback(obj, other, sizeof(obj));
+  EXPECT_TRUE(IsMarkedAsCopy(obj, sizeof(obj)));
+
+  // Modifying `obj` must mark it as non-copy.
+  __copyprof_store_callback(obj + 4, 4);
+  EXPECT_FALSE(IsMarkedAsCopy(obj, sizeof(obj)));
+
+  // After destroying the modified copy, `is_transitive_copy` must be `false`.
+  __copyprof_dtor_enter_callback(obj, sizeof(obj));
+  __copyprof_dtor_exit_callback(obj, sizeof(obj));
+  EXPECT_FALSE(__copyprof_state.is_transitive_copy);
+}
+
+TEST_F(CopyProfStateTest, CheckModeIgnoresStores) {
+  char obj[32] = {0};
+  char other[32] = {0};
+
+  __copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
+  __copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
+  EXPECT_TRUE(IsMarkedAsCopy(obj, sizeof(obj)));
+
+  __copyprof_dtor_enter_callback(obj, sizeof(obj));
+
+  // Simulate an internal store during destruction (e.g. vtable rewrite or
+  // member cleanup).
+  __copyprof_store_callback(obj, sizeof(obj));
+
+  // Prove that stores in CHECK mode do not clear the copy shadow bits.
+  EXPECT_TRUE(IsMarkedAsCopy(obj, sizeof(obj)));
+  __copyprof_dtor_exit_callback(obj, sizeof(obj));
+}
+
+// Tests that the `DTOR` context invariant covers every shadow write, not just
+// stores. A temporary object created inside a d'tor must leave shadow memory
+// alone too.
+TEST_F(CopyProfStateTest, CheckModeIgnoresConstruction) {
+  char obj[32] = {0};
+  char other[32] = {0};
+  char scratch[32] = {0};
+
+  // Mark `scratch` as a copy up front so a stray shadow write is observable.
+  MarkApplicationMemory(scratch, sizeof(scratch), /*is_copy=*/true);
+
+  __copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
+  __copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
+
+  // Enter the d'tor, then construct an object over `scratch`.
+  __copyprof_dtor_enter_callback(obj, sizeof(obj));
+  __copyprof_ctor_enter_callback(scratch, sizeof(scratch));
+  __copyprof_ctor_exit_callback(scratch, sizeof(scratch));
+  // The c'tor must not have modified shadow memory so `scratch` must still be
+  // marked as copy.
+  EXPECT_TRUE(IsMarkedAsCopy(scratch, sizeof(scratch)));
+  __copyprof_dtor_exit_callback(obj, sizeof(obj));
+}
+
+// A d'tor whose body constructs and destroys an object that the destroyed
+// object does not own must not suppress the report. `is_transitive_copy` is
+// folded with `IsMarkedAsCopy` for every nested d'tor exit, with no check that
+// the nested object is reachable from the top-level one, so any local in the
+// d'tor body (a string built for a log message, a lock guard, an iterator)
+// clears it. Contrast with SimulatedUnnecessaryCopy, which is the same
+// scenario with an empty d'tor body and does report.
+//
+// DISABLED: this is a known limitation of the minimal runtime, not a defect to
+// be fixed in isolation. Telling a temporary created *by* the d'tor apart from
+// a sub-object that the destroyed object *owns* requires per-object state keyed
+// by allocation, which arrives with the malloc/new interceptors in a follow-up
+// patch. Restricting the fold to the top-level object's flat extent does make
+// this test pass, but it then misses heap-owned sub-objects with non-trivial
+// d'tors (`std::vector<std::string>`), turning a false negative into a false
+// positive -- the worse trade for this tool. Re-enable once allocation
+// tracking can supply the ownership predicate.
+TEST_F(CopyProfStateTest, DISABLED_TemporaryObjectInDtorDoesNotSuppressReport) {
+  char obj[32] = {0};
+  char source_obj[32] = {0};
+  char local[32] = {0};
+
+  // `obj` is an unnecessary copy: copy-constructed and never modified.
+  __copyprof_copy_ctor_enter_callback(obj, source_obj, sizeof(obj));
+  __copyprof_copy_ctor_exit_callback(obj, source_obj, sizeof(obj));
+  ASSERT_TRUE(IsMarkedAsCopy(obj, sizeof(obj)));
+
+  // Memory occupied by `local` is not a copy.
+  MarkApplicationMemory(local, sizeof(local), /*is_copy=*/false);
+
+  __copyprof_dtor_enter_callback(obj, sizeof(obj));
+  // Create and destroy `local` while `obj`'s d'tor is running.
+  __copyprof_ctor_enter_callback(local, sizeof(local));
+  __copyprof_ctor_exit_callback(local, sizeof(local));
+  __copyprof_dtor_enter_callback(local, sizeof(local));
+  __copyprof_dtor_exit_callback(local, sizeof(local));
+
+  // `obj` itself was never modified, so it must still be marked as copy.
+  __copyprof_dtor_exit_callback(obj, sizeof(obj));
+  EXPECT_TRUE(IsMarkedAsCopy(obj, sizeof(obj)));
+  EXPECT_TRUE(__copyprof_state.is_transitive_copy);
+}
+
+}  // namespace
+}  // namespace __copyprof
diff --git a/compiler-rt/lib/copyprof/tests/driver.cpp b/compiler-rt/lib/copyprof/tests/driver.cpp
new file mode 100644
index 00000000000000..36d40b83a267b2
--- /dev/null
+++ b/compiler-rt/lib/copyprof/tests/driver.cpp
@@ -0,0 +1,19 @@
+//===-- driver.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 standalone test driver main function for running
+/// CopyProf runtime unit tests via Google Test.
+///
+//===----------------------------------------------------------------------===//
+
+#include "gtest/gtest.h"
+
+int main(int argc, char** argv) {
+  testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_internal_defs.h b/compiler-rt/lib/sanitizer_common/sanitizer_internal_defs.h
index e1fc1c6871cf72..b8a6ac95d607b2 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_internal_defs.h
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_internal_defs.h
@@ -501,5 +501,8 @@ using namespace __sanitizer;
 namespace __memprof {
 using namespace __sanitizer;
 }
+namespace __copyprof {
+using namespace __sanitizer;
+}
 
 #endif  // SANITIZER_DEFS_H

>From 599556319deaa2b09afce97cf31e25fec708d97c Mon Sep 17 00:00:00 2001
From: Jan Newger <jannewger at gmail.com>
Date: Tue, 15 Sep 2026 19:00:21 +0000
Subject: [PATCH 2/2] fixup! [CopyProf] Implement minimal CopyProf runtime.

---
 compiler-rt/lib/copyprof/copyprof.cpp                  | 9 ++++-----
 compiler-rt/lib/copyprof/copyprof_interface_internal.h | 2 +-
 compiler-rt/lib/copyprof/copyprof_state.h              | 4 ++--
 compiler-rt/lib/copyprof/tests/driver.cpp              | 2 +-
 4 files changed, 8 insertions(+), 9 deletions(-)

diff --git a/compiler-rt/lib/copyprof/copyprof.cpp b/compiler-rt/lib/copyprof/copyprof.cpp
index 854f9936847ce6..a272cf83075542 100644
--- a/compiler-rt/lib/copyprof/copyprof.cpp
+++ b/compiler-rt/lib/copyprof/copyprof.cpp
@@ -55,10 +55,9 @@ static void MaybeUpdateSmfContext(SmfContext context) {
   // Only entering a top level special member function changes the current
   // context. The context logically remains the same until control flow leaves
   // the top level function.
-  if (__copyprof_state.smf_context == SmfContext::NONE ||
-      (__copyprof_state.construct_nesting_level == 0 &&
-       __copyprof_state.copy_nesting_level == 0 &&
-       __copyprof_state.destruct_nesting_level == 0)) {
+  if (__copyprof_state.construct_nesting_level == 0 &&
+      __copyprof_state.copy_nesting_level == 0 &&
+      __copyprof_state.destruct_nesting_level == 0) {
     __copyprof_state.smf_context = context;
   }
 }
@@ -90,7 +89,7 @@ static void CopyMemberFunctionExit(const void* this_ptr, uptr obj_size) {
 
 }  // namespace __copyprof
 
-void __copyprof_init() { Initialize(); }
+void __copyprof_init_once() { Initialize(); }
 
 void __copyprof_ctor_enter_callback(const void* this_ptr, uptr obj_size) {
   MaybeUpdateSmfContext(SmfContext::CTOR);
diff --git a/compiler-rt/lib/copyprof/copyprof_interface_internal.h b/compiler-rt/lib/copyprof/copyprof_interface_internal.h
index 58e6f84e442bb1..84aeaeb2bdb496 100644
--- a/compiler-rt/lib/copyprof/copyprof_interface_internal.h
+++ b/compiler-rt/lib/copyprof/copyprof_interface_internal.h
@@ -21,7 +21,7 @@ extern "C" {
 
 // Should be called at the very beginning of the process before any instrumented
 // code executes.
-SANITIZER_INTERFACE_ATTRIBUTE void __copyprof_init();
+SANITIZER_INTERFACE_ATTRIBUTE void __copyprof_init_once();
 
 // Runtime callbacks that update the CopyProf state machine and shadow memory
 // when entering or leaving special member functions.
diff --git a/compiler-rt/lib/copyprof/copyprof_state.h b/compiler-rt/lib/copyprof/copyprof_state.h
index c9b2717aa1d254..13cb6a94c38ea3 100644
--- a/compiler-rt/lib/copyprof/copyprof_state.h
+++ b/compiler-rt/lib/copyprof/copyprof_state.h
@@ -62,8 +62,8 @@ struct PerThreadState {
 // The runtime is always linked into the main executable, so the state can be
 // reached with the initial-exec model instead of paying for a __tls_get_addr
 // call on every access.
-__attribute__((tls_model("initial-exec")))
-extern THREADLOCAL PerThreadState __copyprof_state;
+__attribute__((tls_model(
+    "initial-exec"))) extern THREADLOCAL PerThreadState __copyprof_state;
 
 }  // namespace __copyprof
 
diff --git a/compiler-rt/lib/copyprof/tests/driver.cpp b/compiler-rt/lib/copyprof/tests/driver.cpp
index 36d40b83a267b2..f7c3c7b8ef6921 100644
--- a/compiler-rt/lib/copyprof/tests/driver.cpp
+++ b/compiler-rt/lib/copyprof/tests/driver.cpp
@@ -1,4 +1,4 @@
-//===-- driver.cpp ---------------------------------------------------------===//
+//===-- driver.cpp --------------------------------------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.



More information about the llvm-commits mailing list