[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