[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