[llvm-branch-commits] [clang] [compiler-rt] [CopyProf] Intercept new/delete and track per-object cost. (PR #235455)
via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Sun Oct 25 07:43:39 PDT 2026
https://github.com/newgre updated https://github.com/llvm/llvm-project/pull/235455
>From 72bde895a26047d3745a62ab53071f96641473dd Mon Sep 17 00:00:00 2001
From: jannewger <jannewger at gmail.com>
Date: Tue, 20 Oct 2026 22:24:48 +0200
Subject: [PATCH] [CopyProf] Intercept new/delete and track per-object cost.
This intercepts the C++ `new` and `delete` operators, so that all
heap memory owned by a copy is tracked.
Using per-object state tracking, reports now tell the user how much
accumulated memory a copy is worth and whether it allocated.
This is useful for filtering of reports.
RFC: https://discourse.llvm.org/t/rfc-copysanitizer-csan-detecting-unneccessary-object-copies-at-runtime/91038
---
clang/lib/Driver/ToolChains/CommonArgs.cpp | 5 +-
clang/test/Driver/fcopyprof.cpp | 9 +-
compiler-rt/lib/copyprof/CMakeLists.txt | 23 +++
compiler-rt/lib/copyprof/copyprof.cpp | 22 ++-
.../lib/copyprof/copyprof_allocator.cpp | 12 +-
.../lib/copyprof/copyprof_new_delete.cpp | 139 ++++++++++++++++++
.../copyprof/copyprof_per_object_state.cpp | 70 +++++++++
.../lib/copyprof/copyprof_per_object_state.h | 44 ++++++
.../lib/copyprof/copyprof_reporting.cpp | 2 +-
compiler-rt/lib/copyprof/copyprof_reporting.h | 5 +-
compiler-rt/lib/copyprof/copyprof_state.cpp | 10 ++
compiler-rt/lib/copyprof/copyprof_state.h | 15 ++
.../copyprof/tests/copyprof_state_test.cpp | 102 +++++++++++++
13 files changed, 440 insertions(+), 18 deletions(-)
create mode 100644 compiler-rt/lib/copyprof/copyprof_new_delete.cpp
create mode 100644 compiler-rt/lib/copyprof/copyprof_per_object_state.cpp
create mode 100644 compiler-rt/lib/copyprof/copyprof_per_object_state.h
diff --git a/clang/lib/Driver/ToolChains/CommonArgs.cpp b/clang/lib/Driver/ToolChains/CommonArgs.cpp
index 7c832ef405fa51..ef83d55f8344ad 100644
--- a/clang/lib/Driver/ToolChains/CommonArgs.cpp
+++ b/clang/lib/Driver/ToolChains/CommonArgs.cpp
@@ -1795,8 +1795,11 @@ collectSanitizerRuntimes(Compilation &C, const ToolChain &TC,
StaticRuntimes.push_back("dfsan");
if (SanArgs.needsLsanRt())
StaticRuntimes.push_back("lsan");
- if (SanArgs.needsCopyProfRt())
+ if (SanArgs.needsCopyProfRt()) {
StaticRuntimes.push_back("copyprof");
+ if (SanArgs.linkCXXRuntimes())
+ StaticRuntimes.push_back("copyprof_cxx");
+ }
if (SanArgs.needsMsanRt()) {
StaticRuntimes.push_back("msan");
if (SanArgs.linkCXXRuntimes())
diff --git a/clang/test/Driver/fcopyprof.cpp b/clang/test/Driver/fcopyprof.cpp
index 2ac70d3349116e..8e3b5781385770 100644
--- a/clang/test/Driver/fcopyprof.cpp
+++ b/clang/test/Driver/fcopyprof.cpp
@@ -1,13 +1,14 @@
// Test driver flags for CopyProf instrumentation and runtime linking.
// Basic C and C++ invocations:
-// RUN: %clang --target=x86_64-linux-gnu -fcopyprof %s -### 2>&1 | FileCheck %s
-// RUN: %clangxx --target=x86_64-linux-gnu -fcopyprof %s -### 2>&1 | FileCheck %s
+// RUN: %clang --target=x86_64-linux-gnu -fcopyprof %s -### 2>&1 | FileCheck %s --implicit-check-not=libclang_rt.copyprof_cxx
+// RUN: %clangxx --target=x86_64-linux-gnu -fcopyprof %s -### 2>&1 | FileCheck %s --check-prefixes=CHECK,CHECK-CXX
// CHECK: "-cc1" {{.*}} "-fcopyprof"
-// CHECK: ld{{.*}}libclang_rt.copyprof
+// CHECK: ld{{.*}}libclang_rt.copyprof{{[-.]}}
+// CHECK-CXX-SAME: libclang_rt.copyprof_cxx
// Re-enabling after -fno-copyprof:
-// RUN: %clangxx --target=x86_64-linux-gnu -fcopyprof -fno-copyprof -fcopyprof %s -### 2>&1 | FileCheck %s
+// RUN: %clangxx --target=x86_64-linux-gnu -fcopyprof -fno-copyprof -fcopyprof %s -### 2>&1 | FileCheck %s --check-prefixes=CHECK,CHECK-CXX
// Custom static size threshold (and last-flag-wins behavior):
// RUN: %clangxx --target=x86_64-linux-gnu -fcopyprof -fcopyprof-static-size-threshold=32 %s -### 2>&1 | FileCheck %s --check-prefix=THRESHOLD32
diff --git a/compiler-rt/lib/copyprof/CMakeLists.txt b/compiler-rt/lib/copyprof/CMakeLists.txt
index 1d925167f58934..dbbf9ea7a82911 100644
--- a/compiler-rt/lib/copyprof/CMakeLists.txt
+++ b/compiler-rt/lib/copyprof/CMakeLists.txt
@@ -7,18 +7,24 @@ set(COPYPROF_SOURCES
copyprof_interceptors_memintrinsics.cpp
copyprof_malloc_linux.cpp
copyprof_preinit.cpp
+ copyprof_per_object_state.cpp
copyprof_reporting.cpp
copyprof_shadow.cpp
copyprof_stack.cpp
copyprof_state.cpp
)
+set(COPYPROF_CXX_SOURCES
+ copyprof_new_delete.cpp
+)
+
set(COPYPROF_HEADERS
copyprof_allocator.h
copyprof_interceptors.h
copyprof_interceptors_memintrinsics.h
copyprof_interface_internal.h
copyprof_internal.h
+ copyprof_per_object_state.h
copyprof_reporting.h
copyprof_shadow.h
copyprof_state.h
@@ -37,6 +43,12 @@ add_compiler_rt_object_libraries(RTCopyProf
ADDITIONAL_HEADERS ${COPYPROF_HEADERS}
CFLAGS ${COPYPROF_CFLAGS}
DEFS ${COPYPROF_COMMON_DEFINITIONS})
+add_compiler_rt_object_libraries(RTCopyProf_cxx
+ ARCHS ${COPYPROF_SUPPORTED_ARCH}
+ SOURCES ${COPYPROF_CXX_SOURCES}
+ ADDITIONAL_HEADERS ${COPYPROF_HEADERS}
+ CFLAGS ${COPYPROF_CFLAGS}
+ DEFS ${COPYPROF_COMMON_DEFINITIONS})
# Build CopyProf runtimes shipped with Clang.
add_compiler_rt_component(copyprof)
@@ -58,12 +70,23 @@ add_compiler_rt_runtime(clang_rt.copyprof
DEFS ${COPYPROF_COMMON_DEFINITIONS}
PARENT_TARGET copyprof)
+add_compiler_rt_runtime(clang_rt.copyprof_cxx
+ STATIC
+ ARCHS ${COPYPROF_SUPPORTED_ARCH}
+ OBJECT_LIBS RTCopyProf_cxx
+ 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}
PARENT_TARGET copyprof
EXTRA copyprof.syms.extra)
+ add_sanitizer_rt_symbols(clang_rt.copyprof_cxx
+ ARCHS ${arch}
+ PARENT_TARGET copyprof)
endforeach()
endif()
diff --git a/compiler-rt/lib/copyprof/copyprof.cpp b/compiler-rt/lib/copyprof/copyprof.cpp
index 0dddf05af70aa2..29d70e022704b3 100644
--- a/compiler-rt/lib/copyprof/copyprof.cpp
+++ b/compiler-rt/lib/copyprof/copyprof.cpp
@@ -14,6 +14,7 @@
#include "copyprof_allocator.h"
#include "copyprof_interceptors.h"
#include "copyprof_interface_internal.h"
+#include "copyprof_per_object_state.h"
#include "copyprof_reporting.h"
#include "copyprof_shadow.h"
#include "copyprof_state.h"
@@ -53,6 +54,7 @@ static void Initialize() {
InitializeCommonFlags();
InitializeInterceptors();
InitializeShadowMemory();
+ InitializePerObjectTracking();
InitializeCopyProfAllocator();
copyprof_init_is_running = false;
copyprof_is_initialized = true;
@@ -91,7 +93,14 @@ static void MarkConstructedObject(const void* this_ptr, uptr obj_size) {
static void CopyMemberFunctionEnter(const void* this_ptr, uptr obj_size) {
MaybeUpdateSmfContext(SmfContext::COPY);
- if (__copyprof_state.copy_nesting_level++ == 0) {
+ // Currently, only top-level copies are reported, so only top-level objects
+ // get per-object state which is then removed when the corresponding top-level
+ // d'tor exits. A nested object copy is not individually marked as a copy and
+ // is typically destroyed by a nested d'tor, so its state would never be
+ // removed (from the object state map).
+ if (__copyprof_state.copy_nesting_level++ == 0 &&
+ __copyprof_state.smf_context == SmfContext::COPY) {
+ InitPerObjectState(this_ptr, obj_size);
__copyprof_state.current_this_ptr = this_ptr;
}
}
@@ -161,10 +170,13 @@ void __copyprof_dtor_exit_callback(const void* this_ptr, uptr obj_size) {
__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);
+ LogCopyProfReport(GET_CALLER_PC(), GET_CURRENT_FRAME(),
+ GetPerObjectTrackedSize(this_ptr),
+ GetPerObjectDidAllocate(this_ptr));
+ }
+ // Clean up per-object state when top-level destructor exits.
+ if (__copyprof_state.destruct_nesting_level == 0) {
+ RemovePerObjectState(this_ptr);
}
}
diff --git a/compiler-rt/lib/copyprof/copyprof_allocator.cpp b/compiler-rt/lib/copyprof/copyprof_allocator.cpp
index f854a03a4dce58..756a7266868c72 100644
--- a/compiler-rt/lib/copyprof/copyprof_allocator.cpp
+++ b/compiler-rt/lib/copyprof/copyprof_allocator.cpp
@@ -17,6 +17,7 @@
#include "copyprof_interceptors.h"
#include "copyprof_interface_internal.h"
+#include "copyprof_per_object_state.h"
#include "copyprof_shadow.h"
#include "copyprof_state.h"
#include "sanitizer_common/sanitizer_allocator.h"
@@ -152,9 +153,10 @@ struct CopyProfAllocator {
ReportOutOfMemory(size, &stack);
}
GetMetadata(allocated)->requested_size = size;
- MarkApplicationMemory(
- allocated, size,
- ShouldMarkNewMemoryAsCopy(__copyprof_state.smf_context));
+ SmfContext ctx = __copyprof_state.smf_context;
+ MarkApplicationMemory(allocated, size, ShouldMarkNewMemoryAsCopy(ctx));
+ if (ctx == SmfContext::COPY)
+ RecordPerObjectAllocation(__copyprof_state.current_this_ptr, size);
return allocated;
}
@@ -191,8 +193,8 @@ struct CopyProfAllocator {
}
// Implemented via `Allocate` and `Deallocate` rather than
- // `CombinedAllocator::Reallocate`, s.t. shadow memory and metadata are
- // updated as for any other (de)allocation.
+ // `CombinedAllocator::Reallocate`, s.t. shadow memory, metadata and
+ // per-object state are updated as for any other (de)allocation.
// Note that the shadow of the old memory is not carried over.
// TODO: figure out whether this assumption is a problem in practice.
void* Reallocate(void* old_ptr, uptr new_size) {
diff --git a/compiler-rt/lib/copyprof/copyprof_new_delete.cpp b/compiler-rt/lib/copyprof/copyprof_new_delete.cpp
new file mode 100644
index 00000000000000..b91c08a9249651
--- /dev/null
+++ b/compiler-rt/lib/copyprof/copyprof_new_delete.cpp
@@ -0,0 +1,139 @@
+//===-- copyprof_new_delete.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 interceptors for C++ new and delete operators, tracking
+/// dynamic object lifecycle events in the CopyProf runtime.
+///
+//===----------------------------------------------------------------------===//
+
+// Routines to intercept C and C++ (de-)allocation functions.
+
+#include <stddef.h>
+
+#include "copyprof_allocator.h"
+#include "interception/interception.h"
+#include "sanitizer_common/sanitizer_allocator.h"
+#include "sanitizer_common/sanitizer_allocator_report.h"
+#include "sanitizer_common/sanitizer_common.h"
+#include "sanitizer_common/sanitizer_flags.h"
+#include "sanitizer_common/sanitizer_internal_defs.h"
+#include "sanitizer_common/sanitizer_stacktrace.h"
+
+// Fake std::nothrow_t and std::align_val_t to avoid including <new>.
+namespace std {
+struct nothrow_t {};
+enum class align_val_t : size_t {};
+} // namespace std
+
+using namespace __copyprof;
+
+ALWAYS_INLINE
+void* OperatorNew(size_t size, std::align_val_t align, bool no_throw) {
+ void* result = CopyProfAllocate(size, static_cast<uptr>(align));
+ if (!no_throw && UNLIKELY(!result)) {
+ UNINITIALIZED BufferedStackTrace stack;
+ stack.Unwind(StackTrace::GetCurrentPc(), GET_CURRENT_FRAME(),
+ /*context=*/nullptr, common_flags()->fast_unwind_on_fatal);
+ ReportOutOfMemory(size, &stack);
+ }
+ return result;
+}
+
+ALWAYS_INLINE
+void OperatorDelete(void* ptr, size_t size, std::align_val_t align) {
+ CopyProfDeallocate(ptr, size, static_cast<uptr>(align));
+}
+
+INTERCEPTOR_ATTRIBUTE
+void* operator new(size_t size) {
+ return OperatorNew(size, std::align_val_t{kMinAlignment},
+ /*no_throw=*/false);
+}
+INTERCEPTOR_ATTRIBUTE
+void* operator new(size_t size, std::align_val_t align) {
+ return OperatorNew(size, align, /*no_throw=*/false);
+}
+INTERCEPTOR_ATTRIBUTE
+void* operator new(size_t size, std::nothrow_t const&) {
+ return OperatorNew(size, std::align_val_t{kMinAlignment},
+ /*no_throw=*/true);
+}
+INTERCEPTOR_ATTRIBUTE
+void* operator new(size_t size, std::align_val_t align, std::nothrow_t const&) {
+ return OperatorNew(size, align, /*no_throw=*/true);
+}
+INTERCEPTOR_ATTRIBUTE
+void* operator new[](size_t size) {
+ return OperatorNew(size, std::align_val_t{kMinAlignment},
+ /*no_throw=*/false);
+}
+INTERCEPTOR_ATTRIBUTE
+void* operator new[](size_t size, std::align_val_t align) {
+ return OperatorNew(size, align, /*no_throw=*/false);
+}
+INTERCEPTOR_ATTRIBUTE
+void* operator new[](size_t size, std::nothrow_t const&) {
+ return OperatorNew(size, std::align_val_t{kMinAlignment},
+ /*no_throw=*/true);
+}
+INTERCEPTOR_ATTRIBUTE
+void* operator new[](size_t size, std::align_val_t align,
+ std::nothrow_t const&) {
+ return OperatorNew(size, align, /*no_throw=*/true);
+}
+
+INTERCEPTOR_ATTRIBUTE
+void operator delete(void* ptr) NOEXCEPT {
+ return OperatorDelete(ptr, /*size=*/0, std::align_val_t{0});
+}
+INTERCEPTOR_ATTRIBUTE
+void operator delete(void* ptr, size_t size) NOEXCEPT {
+ return OperatorDelete(ptr, size, std::align_val_t{0});
+}
+INTERCEPTOR_ATTRIBUTE
+void operator delete(void* ptr, std::align_val_t align) NOEXCEPT {
+ return OperatorDelete(ptr, /*size=*/0, align);
+}
+INTERCEPTOR_ATTRIBUTE
+void operator delete(void* ptr, std::nothrow_t const&) {
+ return OperatorDelete(ptr, /*size=*/0, std::align_val_t{0});
+}
+INTERCEPTOR_ATTRIBUTE
+void operator delete(void* ptr, std::align_val_t align, std::nothrow_t const&) {
+ return OperatorDelete(ptr, /*size=*/0, align);
+}
+INTERCEPTOR_ATTRIBUTE
+void operator delete(void* ptr, size_t size, std::align_val_t align) NOEXCEPT {
+ return OperatorDelete(ptr, size, align);
+}
+INTERCEPTOR_ATTRIBUTE
+void operator delete[](void* ptr) NOEXCEPT {
+ return OperatorDelete(ptr, /*size=*/0, std::align_val_t{0});
+}
+INTERCEPTOR_ATTRIBUTE
+void operator delete[](void* ptr, size_t size) NOEXCEPT {
+ return OperatorDelete(ptr, size, std::align_val_t{0});
+}
+INTERCEPTOR_ATTRIBUTE
+void operator delete[](void* ptr, std::align_val_t align) NOEXCEPT {
+ return OperatorDelete(ptr, /*size=*/0, align);
+}
+INTERCEPTOR_ATTRIBUTE
+void operator delete[](void* ptr, std::nothrow_t const&) {
+ return OperatorDelete(ptr, /*size=*/0, std::align_val_t{0});
+}
+INTERCEPTOR_ATTRIBUTE
+void operator delete[](void* ptr, std::align_val_t align,
+ std::nothrow_t const&) {
+ return OperatorDelete(ptr, /*size=*/0, align);
+}
+INTERCEPTOR_ATTRIBUTE
+void operator delete[](void* ptr, size_t size,
+ std::align_val_t align) NOEXCEPT {
+ return OperatorDelete(ptr, size, align);
+}
diff --git a/compiler-rt/lib/copyprof/copyprof_per_object_state.cpp b/compiler-rt/lib/copyprof/copyprof_per_object_state.cpp
new file mode 100644
index 00000000000000..9b3608a6fcc1a3
--- /dev/null
+++ b/compiler-rt/lib/copyprof/copyprof_per_object_state.cpp
@@ -0,0 +1,70 @@
+//===-- copyprof_per_object_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 per-object state tracking utilities for CopyProf,
+/// managing transitive object sizes and allocation flags.
+///
+//===----------------------------------------------------------------------===//
+
+#include "copyprof_per_object_state.h"
+
+#include "copyprof_state.h"
+#include "sanitizer_common/sanitizer_dense_map.h"
+#include "sanitizer_common/sanitizer_mutex.h"
+#include "sanitizer_common/sanitizer_placement_new.h"
+
+namespace __copyprof {
+namespace {
+
+using PerObjMap = DenseMap<const void*, PerObjectState>;
+
+// Global map for per-object size tracking.
+// Protected by mutex for thread-safe access.
+StaticSpinMutex g_per_obj_map_mutex;
+alignas(PerObjMap) char g_per_obj_map_placeholder[sizeof(PerObjMap)];
+PerObjMap* g_per_obj_map = nullptr;
+
+} // namespace
+
+void InitializePerObjectTracking() {
+ g_per_obj_map_mutex.Init();
+ g_per_obj_map = new (g_per_obj_map_placeholder) PerObjMap();
+}
+
+void InitPerObjectState(const void* this_ptr, uptr static_size) {
+ SpinMutexLock lock(&g_per_obj_map_mutex);
+ PerObjectState& state = (*g_per_obj_map)[this_ptr];
+ state.SetStaticSize(static_size);
+ state.did_allocate = false;
+}
+
+void RecordPerObjectAllocation(const void* this_ptr, uptr num_bytes) {
+ SpinMutexLock lock(&g_per_obj_map_mutex);
+ PerObjectState& state = (*g_per_obj_map)[this_ptr];
+ state.IncreaseTrackedSize(num_bytes);
+ state.did_allocate = true;
+}
+
+uptr GetPerObjectTrackedSize(const void* this_ptr) {
+ SpinMutexLock lock(&g_per_obj_map_mutex);
+ const auto* it = g_per_obj_map->find(this_ptr);
+ return it ? it->second.GetTrackedSize() : 0;
+}
+
+bool GetPerObjectDidAllocate(const void* this_ptr) {
+ SpinMutexLock lock(&g_per_obj_map_mutex);
+ const auto* it = g_per_obj_map->find(this_ptr);
+ return it ? it->second.did_allocate : false;
+}
+
+void RemovePerObjectState(const void* this_ptr) {
+ SpinMutexLock lock(&g_per_obj_map_mutex);
+ g_per_obj_map->erase(this_ptr);
+}
+
+} // namespace __copyprof
diff --git a/compiler-rt/lib/copyprof/copyprof_per_object_state.h b/compiler-rt/lib/copyprof/copyprof_per_object_state.h
new file mode 100644
index 00000000000000..09a694748848f1
--- /dev/null
+++ b/compiler-rt/lib/copyprof/copyprof_per_object_state.h
@@ -0,0 +1,44 @@
+//===-- copyprof_per_object_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 per-object state data structures and helper functions
+/// for tracking dynamic memory allocations associated with object copies.
+///
+//===----------------------------------------------------------------------===//
+
+#ifndef COPYPROF_PER_OBJECT_STATE_H
+#define COPYPROF_PER_OBJECT_STATE_H
+
+#include "sanitizer_common/sanitizer_internal_defs.h"
+
+namespace __copyprof {
+
+// Initializes the per-object state tracking.
+// Must be called once at program startup, after flags are initialized.
+void InitializePerObjectTracking();
+
+// Initializes the per-object state for the object at `this_ptr` with the given
+// static size.
+void InitPerObjectState(const void* this_ptr, uptr static_size);
+
+// Records a dynamic allocation of `num_bytes` for the object at `this_ptr`.
+void RecordPerObjectAllocation(const void* this_ptr, uptr num_bytes);
+
+// Returns the tracked size for the object at `this_ptr`.
+uptr GetPerObjectTrackedSize(const void* this_ptr);
+
+// Returns whether the object at `this_ptr` has performed any dynamic
+// allocations.
+bool GetPerObjectDidAllocate(const void* this_ptr);
+
+// Removes the per-object state for the object at `this_ptr`.
+void RemovePerObjectState(const void* this_ptr);
+
+} // namespace __copyprof
+
+#endif // COPYPROF_PER_OBJECT_STATE_H
diff --git a/compiler-rt/lib/copyprof/copyprof_reporting.cpp b/compiler-rt/lib/copyprof/copyprof_reporting.cpp
index 439438bc12c842..2a1d6162a8792e 100644
--- a/compiler-rt/lib/copyprof/copyprof_reporting.cpp
+++ b/compiler-rt/lib/copyprof/copyprof_reporting.cpp
@@ -13,7 +13,6 @@
#include "copyprof_reporting.h"
-#include "copyprof_internal.h"
#include "sanitizer_common/sanitizer_common.h"
#include "sanitizer_common/sanitizer_stacktrace.h"
@@ -39,4 +38,5 @@ void LogCopyProfReport(uptr pc, uptr bp, uptr obj_size, bool did_allocate) {
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
index 17b5be426346fd..a2aeb166abe433 100644
--- a/compiler-rt/lib/copyprof/copyprof_reporting.h
+++ b/compiler-rt/lib/copyprof/copyprof_reporting.h
@@ -19,8 +19,9 @@
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.
+// and frame pointer where the copy was destroyed, `obj_size` its size in bytes
+// (including memory allocated while copying it), and `did_allocate` whether
+// the copy allocated memory.
void LogCopyProfReport(uptr pc, uptr bp, uptr obj_size, bool did_allocate);
} // namespace __copyprof
diff --git a/compiler-rt/lib/copyprof/copyprof_state.cpp b/compiler-rt/lib/copyprof/copyprof_state.cpp
index 4da7b1f77ff1a8..83737d9fd9597f 100644
--- a/compiler-rt/lib/copyprof/copyprof_state.cpp
+++ b/compiler-rt/lib/copyprof/copyprof_state.cpp
@@ -17,4 +17,14 @@ namespace __copyprof {
THREADLOCAL PerThreadState __copyprof_state;
+void PerObjectState::SetStaticSize(usize num_bytes) {
+ tracked_size = num_bytes;
+}
+
+void PerObjectState::IncreaseTrackedSize(usize num_bytes) {
+ tracked_size += num_bytes;
+}
+
+usize PerObjectState::GetTrackedSize() const { return tracked_size; }
+
} // namespace __copyprof
diff --git a/compiler-rt/lib/copyprof/copyprof_state.h b/compiler-rt/lib/copyprof/copyprof_state.h
index e8b5f3bf2b0b4d..327af37ab93c6e 100644
--- a/compiler-rt/lib/copyprof/copyprof_state.h
+++ b/compiler-rt/lib/copyprof/copyprof_state.h
@@ -72,6 +72,21 @@ struct PerThreadState {
const void* current_this_ptr = nullptr;
};
+// Each copied object has an associated `PerObjectState`.
+// Used for tracking the transitive size (size of self + all memory allocations)
+// of the object.
+struct PerObjectState {
+ void SetStaticSize(usize num_bytes);
+ void IncreaseTrackedSize(usize num_bytes);
+ usize GetTrackedSize() const;
+
+ // Whether memory was dynamically allocated while making the copy.
+ bool did_allocate = false;
+
+ // Total transitive size (self + all memory allocations) in bytes.
+ usize tracked_size = 0;
+};
+
// 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.
diff --git a/compiler-rt/lib/copyprof/tests/copyprof_state_test.cpp b/compiler-rt/lib/copyprof/tests/copyprof_state_test.cpp
index 2ef72ebf44cebb..736958be9d3f58 100644
--- a/compiler-rt/lib/copyprof/tests/copyprof_state_test.cpp
+++ b/compiler-rt/lib/copyprof/tests/copyprof_state_test.cpp
@@ -10,8 +10,10 @@
#include "copyprof/copyprof_allocator.h"
#include "copyprof/copyprof_interface_internal.h"
+#include "copyprof/copyprof_per_object_state.h"
#include "copyprof/copyprof_shadow.h"
#include "gtest/gtest.h"
+#include "sanitizer_common/sanitizer_allocator.h"
namespace __copyprof {
namespace {
@@ -30,6 +32,7 @@ class CopyProfStateTest : public testing::Test {
void SetUp() override {
__copyprof_init_once();
__copyprof_state = PerThreadState();
+ InitializePerObjectTracking();
}
};
@@ -242,6 +245,105 @@ TEST_F(CopyProfStateTest, DISABLED_MovedTemporaryInDtorDoesNotSuppressReport) {
EXPECT_TRUE(__copyprof_state.is_transitive_copy);
}
+TEST_F(CopyProfStateTest, PerObjectStateTrackingWorksCorrectly) {
+ char obj[24] = {0};
+
+ // Querying an untracked object returns 0 / false.
+ EXPECT_EQ(GetPerObjectTrackedSize(obj), 0);
+ EXPECT_FALSE(GetPerObjectDidAllocate(obj));
+
+ InitPerObjectState(obj, sizeof(obj));
+ EXPECT_EQ(GetPerObjectTrackedSize(obj), sizeof(obj));
+ EXPECT_FALSE(GetPerObjectDidAllocate(obj));
+
+ RecordPerObjectAllocation(obj, /*num_bytes=*/77);
+ EXPECT_EQ(GetPerObjectTrackedSize(obj), sizeof(obj) + 77);
+ EXPECT_TRUE(GetPerObjectDidAllocate(obj));
+
+ RecordPerObjectAllocation(obj, /*num_bytes=*/32);
+ EXPECT_EQ(GetPerObjectTrackedSize(obj), sizeof(obj) + 77 + 32);
+
+ RemovePerObjectState(obj);
+ EXPECT_EQ(GetPerObjectTrackedSize(obj), 0);
+ EXPECT_FALSE(GetPerObjectDidAllocate(obj));
+}
+
+// Only a top-level copy gets per-object state, which is removed again when the
+// top-level d'tor exits. A copy nested in a c'tor (e.g. a member copied from a
+// c'tor argument) is eventually destroyed by a nested d'tor, so state created
+// for it would never be removed.
+TEST_F(CopyProfStateTest, CopyNestedInConstructorHasNoPerObjectState) {
+ char obj[32] = {0};
+ char other[16] = {0};
+ char* member = obj + 16;
+
+ __copyprof_ctor_enter_callback(obj, sizeof(obj));
+ __copyprof_copy_ctor_enter_callback(member, other, sizeof(other));
+ __copyprof_copy_ctor_exit_callback(member, other, sizeof(other));
+ __copyprof_ctor_exit_callback(obj, sizeof(obj));
+
+ EXPECT_EQ(GetPerObjectTrackedSize(member), 0u);
+}
+
+// Likewise for a copy nested in a d'tor, e.g. a temporary created by its body.
+TEST_F(CopyProfStateTest, CopyNestedInDestructorHasNoPerObjectState) {
+ char obj[16] = {0};
+ char temp[16] = {0};
+ char other[16] = {0};
+
+ __copyprof_dtor_enter_callback(obj, sizeof(obj));
+ __copyprof_copy_ctor_enter_callback(temp, other, sizeof(temp));
+ __copyprof_copy_ctor_exit_callback(temp, other, sizeof(temp));
+ __copyprof_dtor_enter_callback(temp, sizeof(temp));
+ __copyprof_dtor_exit_callback(temp, sizeof(temp));
+ __copyprof_dtor_exit_callback(obj, sizeof(obj));
+
+ EXPECT_EQ(GetPerObjectTrackedSize(temp), 0u);
+}
+
+TEST_F(CopyProfStateTest, AllocationDuringCopyIsRecordedInPerObjectState) {
+ char obj[16] = {0};
+ char other[16] = {0};
+
+ __copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
+ void* buf = CopyProfAllocate(/*size=*/32, /*align=*/8);
+ __copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
+
+ ASSERT_NE(buf, nullptr);
+ EXPECT_EQ(GetPerObjectTrackedSize(obj), sizeof(obj) + 32);
+ EXPECT_TRUE(GetPerObjectDidAllocate(obj));
+ CopyProfDeallocate(buf, /*size=*/32, /*align=*/8);
+
+ // Destroying `obj` must erase its object state and hence the tracked size
+ // must be zero.
+ __copyprof_dtor_enter_callback(obj, sizeof(obj));
+ __copyprof_dtor_exit_callback(obj, sizeof(obj));
+ EXPECT_EQ(GetPerObjectTrackedSize(obj), 0);
+}
+
+TEST_F(CopyProfStateTest, FailedAllocationDuringCopyIsNotRecorded) {
+ char obj[16] = {0};
+ char other[16] = {0};
+ const bool may_return_null = AllocatorMayReturnNull();
+ SetAllocatorMayReturnNull(true);
+
+ __copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
+ // A non-power-of-two alignment makes the allocation fail.
+ void* buf = CopyProfAllocate(/*size=*/32, /*align=*/3);
+ __copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
+ SetAllocatorMayReturnNull(may_return_null);
+
+ EXPECT_EQ(buf, nullptr);
+ EXPECT_EQ(GetPerObjectTrackedSize(obj), sizeof(obj));
+ EXPECT_FALSE(GetPerObjectDidAllocate(obj));
+
+ // Destroying `obj` must erase its object state and hence the tracked size
+ // must be zero.
+ __copyprof_dtor_enter_callback(obj, sizeof(obj));
+ __copyprof_dtor_exit_callback(obj, sizeof(obj));
+ EXPECT_EQ(GetPerObjectTrackedSize(obj), 0);
+}
+
TEST_F(CopyProfStateTest,
DeallocatingUnmodifiedCopyAllocationPreservesTransitiveCopy) {
char obj[16] = {0};
More information about the llvm-branch-commits
mailing list