[llvm-branch-commits] [compiler-rt] [CopyProf] Intercept C heap functions. (PR #235454)
via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Sun Oct 25 07:43:38 PDT 2026
https://github.com/newgre updated https://github.com/llvm/llvm-project/pull/235454
>From c04b418b1a77306ce174ac0cafd2e983c04ce9c2 Mon Sep 17 00:00:00 2001
From: jannewger <jannewger at gmail.com>
Date: Tue, 20 Oct 2026 22:23:42 +0200
Subject: [PATCH] [CopyProf] Intercept C heap functions.
A copy that owns heap memory is only unnecessary if that memory is not
modified either. This intercepts the libc heap functions and routes them
to the CopyProf allocator, which marks memory allocated while copying as
copy, and checks the shadow memory of memory freed during destruction.
Memory allocated inside a d'tor is marked as copy as well, so that
freeing temporaries does not suppress reports.
Now that `malloc` is intercepted, `strndup` and `realpath` are
intercepted as well.
RFC: https://discourse.llvm.org/t/rfc-copysanitizer-csan-detecting-unneccessary-object-copies-at-runtime/91038
---
compiler-rt/lib/copyprof/CMakeLists.txt | 1 +
.../lib/copyprof/copyprof_allocator.cpp | 19 +-
.../lib/copyprof/copyprof_interceptors.cpp | 9 +-
.../lib/copyprof/copyprof_interceptors.h | 4 +
compiler-rt/lib/copyprof/copyprof_internal.h | 4 +
.../lib/copyprof/copyprof_malloc_linux.cpp | 182 ++++++++++++++++
compiler-rt/lib/copyprof/copyprof_state.h | 18 +-
.../copyprof/tests/copyprof_state_test.cpp | 197 ++++++++++++++++++
8 files changed, 415 insertions(+), 19 deletions(-)
create mode 100644 compiler-rt/lib/copyprof/copyprof_malloc_linux.cpp
diff --git a/compiler-rt/lib/copyprof/CMakeLists.txt b/compiler-rt/lib/copyprof/CMakeLists.txt
index c0ded0392c0b9a..1d925167f58934 100644
--- a/compiler-rt/lib/copyprof/CMakeLists.txt
+++ b/compiler-rt/lib/copyprof/CMakeLists.txt
@@ -5,6 +5,7 @@ set(COPYPROF_SOURCES
copyprof_allocator.cpp
copyprof_interceptors.cpp
copyprof_interceptors_memintrinsics.cpp
+ copyprof_malloc_linux.cpp
copyprof_preinit.cpp
copyprof_reporting.cpp
copyprof_shadow.cpp
diff --git a/compiler-rt/lib/copyprof/copyprof_allocator.cpp b/compiler-rt/lib/copyprof/copyprof_allocator.cpp
index f5baedb67ed9a8..f854a03a4dce58 100644
--- a/compiler-rt/lib/copyprof/copyprof_allocator.cpp
+++ b/compiler-rt/lib/copyprof/copyprof_allocator.cpp
@@ -18,6 +18,7 @@
#include "copyprof_interceptors.h"
#include "copyprof_interface_internal.h"
#include "copyprof_shadow.h"
+#include "copyprof_state.h"
#include "sanitizer_common/sanitizer_allocator.h"
#include "sanitizer_common/sanitizer_allocator_checks.h"
#include "sanitizer_common/sanitizer_allocator_interface.h"
@@ -36,7 +37,8 @@ using namespace __sanitizer;
// Callbacks invoked by the sanitizer allocator whenever it maps or unmaps
// memory. They are no-ops, as CopyProf does not need to track the allocator's
-// mappings.
+// mappings: shadow memory is updated per allocation instead (see
+// `CopyProfAllocator::Allocate`).
struct CopyProfMapUnmapCallback {
// Called when the primary allocator maps memory for a region from which it
// serves small and medium sized allocations.
@@ -100,7 +102,7 @@ AllocatorCache* GetThreadCache() {
return thread_cache;
}
-// Custom allocator that keeps track of allocation sizes.
+// Custom allocator that updates shadow memory accordingly.
struct CopyProfAllocator {
// Heap allocations must be at least 8-byte aligned so independent
// allocations never share a shadow byte (avoiding data races).
@@ -150,12 +152,19 @@ struct CopyProfAllocator {
ReportOutOfMemory(size, &stack);
}
GetMetadata(allocated)->requested_size = size;
+ MarkApplicationMemory(
+ allocated, size,
+ ShouldMarkNewMemoryAsCopy(__copyprof_state.smf_context));
return allocated;
}
void Deallocate(void* ptr) {
if (UNLIKELY(!ptr))
return;
+ if (__copyprof_state.smf_context == SmfContext::DTOR) {
+ __copyprof_state.is_transitive_copy &=
+ IsMarkedAsCopy(ptr, GetMetadata(ptr)->requested_size);
+ }
if (AllocatorCache* cache = GetThreadCache()) {
allocator.Deallocate(cache, ptr);
} else {
@@ -182,8 +191,10 @@ struct CopyProfAllocator {
}
// Implemented via `Allocate` and `Deallocate` rather than
- // `CombinedAllocator::Reallocate`, s.t. metadata is updated as for any other
- // (de)allocation.
+ // `CombinedAllocator::Reallocate`, s.t. shadow memory and metadata 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) {
CHECK(old_ptr && new_size);
void* new_ptr = Allocate(new_size);
diff --git a/compiler-rt/lib/copyprof/copyprof_interceptors.cpp b/compiler-rt/lib/copyprof/copyprof_interceptors.cpp
index 13ccc0de035861..48eca54249f9a6 100644
--- a/compiler-rt/lib/copyprof/copyprof_interceptors.cpp
+++ b/compiler-rt/lib/copyprof/copyprof_interceptors.cpp
@@ -16,6 +16,7 @@
#include "copyprof/copyprof_interface_internal.h"
#include "copyprof/copyprof_internal.h"
#include "interception/interception.h"
+#include "sanitizer_common/sanitizer_allocator_dlsym.h"
#include "sanitizer_common/sanitizer_common.h"
#include "sanitizer_common/sanitizer_flags.h"
#include "sanitizer_common/sanitizer_internal_defs.h"
@@ -91,13 +92,6 @@ int CopyProfOnExit() { return 0; }
#define COMMON_INTERCEPTOR_NOTHING_IS_INITIALIZED (!copyprof_is_initialized)
-#include "sanitizer_common/sanitizer_platform_interceptors.h"
-// These interceptors allocate memory via `WRAP(malloc)`, but heap functions are
-// not intercepted yet.
-#undef SANITIZER_INTERCEPT_STRNDUP
-#undef SANITIZER_INTERCEPT___STRNDUP
-#undef SANITIZER_INTERCEPT_REALPATH
-
#include "sanitizer_common/sanitizer_common_interceptors.inc"
namespace __copyprof {
@@ -106,6 +100,7 @@ void InitializeInterceptors() {
static bool initialized;
CHECK(!initialized);
initialized = true;
+ InitializeHeapInterceptors();
InitializeCommonInterceptors();
}
diff --git a/compiler-rt/lib/copyprof/copyprof_interceptors.h b/compiler-rt/lib/copyprof/copyprof_interceptors.h
index 9a2fa9ca8ba9c9..10451bda6712ad 100644
--- a/compiler-rt/lib/copyprof/copyprof_interceptors.h
+++ b/compiler-rt/lib/copyprof/copyprof_interceptors.h
@@ -20,6 +20,10 @@
DECLARE_REAL(void*, memset, void* block, int c, SIZE_T size)
DECLARE_REAL(void*, memcpy, void* to, const void* from, SIZE_T size)
+// Functions whose interception and interceptor are defined in another TU.
+DECLARE_REAL_AND_INTERCEPTOR(void*, malloc, SIZE_T)
+DECLARE_REAL_AND_INTERCEPTOR(void, free, void*)
+
// The macros below must be defined when using the interceptor infrastructure
// (i.e. sanitizer_common_interceptors*.inc files).
#define COPYPROF_INTERCEPT_FUNC(name) \
diff --git a/compiler-rt/lib/copyprof/copyprof_internal.h b/compiler-rt/lib/copyprof/copyprof_internal.h
index ccd9c95fe4dd43..701ead441a375e 100644
--- a/compiler-rt/lib/copyprof/copyprof_internal.h
+++ b/compiler-rt/lib/copyprof/copyprof_internal.h
@@ -28,6 +28,10 @@ using __sanitizer::BufferedStackTrace;
extern bool copyprof_is_initialized;
extern bool copyprof_init_is_running;
+// Initializes interceptors for libc heap functions such as `malloc`, `free`,
+// or similar.
+void InitializeHeapInterceptors();
+
} // namespace __copyprof
#endif // COPYPROF_INTERNAL_H
diff --git a/compiler-rt/lib/copyprof/copyprof_malloc_linux.cpp b/compiler-rt/lib/copyprof/copyprof_malloc_linux.cpp
new file mode 100644
index 00000000000000..bc26dd5439fc0b
--- /dev/null
+++ b/compiler-rt/lib/copyprof/copyprof_malloc_linux.cpp
@@ -0,0 +1,182 @@
+//===-- copyprof_malloc_linux.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 Linux-specific malloc and memory allocation
+/// interceptors for CopyProf, wrapping application memory management calls.
+///
+//===----------------------------------------------------------------------===//
+
+#include "copyprof/copyprof_interface_internal.h"
+#include "copyprof/copyprof_internal.h"
+#include "copyprof_allocator.h"
+#include "copyprof_interceptors.h"
+#include "interception/interception.h"
+#include "sanitizer_common/sanitizer_allocator_dlsym.h"
+#include "sanitizer_common/sanitizer_platform_interceptors.h"
+
+using namespace __copyprof;
+using namespace __sanitizer;
+
+namespace __copyprof {
+
+// This is needed to make the heap interceptors re-entrancy safe during the
+// startup phase as `dlsym` itself might allocate.
+struct DlsymAlloc : public DlSymAllocator<DlsymAlloc> {
+ static bool UseImpl() { return copyprof_init_is_running; }
+};
+
+} // namespace __copyprof
+
+#define ENSURE_COPYPROF_INITIALIZED() \
+ do { \
+ if (UNLIKELY(!copyprof_is_initialized)) { \
+ __copyprof_init_once(); \
+ } \
+ } while (0)
+
+INTERCEPTOR(void*, malloc, SIZE_T size) {
+ if (DlsymAlloc::Use())
+ return DlsymAlloc::Allocate(size);
+ ENSURE_COPYPROF_INITIALIZED();
+ return CopyProfMalloc(size);
+}
+
+INTERCEPTOR(void*, calloc, SIZE_T nmemb, SIZE_T size) {
+ if (DlsymAlloc::Use())
+ return DlsymAlloc::Callocate(nmemb, size);
+ ENSURE_COPYPROF_INITIALIZED();
+ return CopyProfCalloc(nmemb, size);
+}
+
+INTERCEPTOR(void*, realloc, void* ptr, SIZE_T size) {
+ if (DlsymAlloc::Use() || DlsymAlloc::PointerIsMine(ptr))
+ return DlsymAlloc::Realloc(ptr, size);
+ ENSURE_COPYPROF_INITIALIZED();
+ return CopyProfRealloc(ptr, size);
+}
+
+INTERCEPTOR(void, free, void* ptr) {
+ if (UNLIKELY(!ptr))
+ return;
+ if (DlsymAlloc::PointerIsMine(ptr))
+ return DlsymAlloc::Free(ptr);
+ ENSURE_COPYPROF_INITIALIZED();
+ CopyProfFree(ptr);
+}
+
+// `free` and `realloc` only accept memory from the CopyProf allocator (or
+// `DlsymAlloc`), so all other libc heap functions are replaced as well.
+
+#if SANITIZER_INTERCEPT_CFREE
+INTERCEPTOR(void, cfree, void* ptr) { WRAP(free)(ptr); }
+#endif
+
+#if SANITIZER_INTERCEPT_FREE_SIZED
+INTERCEPTOR(void, free_sized, void* ptr, SIZE_T size) { WRAP(free)(ptr); }
+#endif
+
+#if SANITIZER_INTERCEPT_FREE_ALIGNED_SIZED
+INTERCEPTOR(void, free_aligned_sized, void* ptr, SIZE_T alignment,
+ SIZE_T size) {
+ WRAP(free)(ptr);
+}
+#endif
+
+#if SANITIZER_INTERCEPT_REALLOCARRAY
+INTERCEPTOR(void*, reallocarray, void* ptr, SIZE_T nmemb, SIZE_T size) {
+ ENSURE_COPYPROF_INITIALIZED();
+ return CopyProfReallocarray(ptr, nmemb, size);
+}
+#endif
+
+#if SANITIZER_INTERCEPT_MEMALIGN
+INTERCEPTOR(void*, memalign, SIZE_T alignment, SIZE_T size) {
+ ENSURE_COPYPROF_INITIALIZED();
+ return CopyProfAllocate(size, alignment);
+}
+#endif
+
+#if SANITIZER_INTERCEPT___LIBC_MEMALIGN
+INTERCEPTOR(void*, __libc_memalign, SIZE_T alignment, SIZE_T size) {
+ ENSURE_COPYPROF_INITIALIZED();
+ return CopyProfAllocate(size, alignment);
+}
+#endif
+
+#if SANITIZER_INTERCEPT_ALIGNED_ALLOC
+// Unlike other sanitizers, `size` need not be a multiple of `alignment`: C17
+// dropped that requirement, and glibc does not enforce it either.
+INTERCEPTOR(void*, aligned_alloc, SIZE_T alignment, SIZE_T size) {
+ ENSURE_COPYPROF_INITIALIZED();
+ return CopyProfAllocate(size, alignment);
+}
+#endif
+
+INTERCEPTOR(int, posix_memalign, void** memptr, SIZE_T alignment, SIZE_T size) {
+ ENSURE_COPYPROF_INITIALIZED();
+ return CopyProfPosixMemalign(memptr, alignment, size);
+}
+
+INTERCEPTOR(void*, valloc, SIZE_T size) {
+ ENSURE_COPYPROF_INITIALIZED();
+ return CopyProfValloc(size);
+}
+
+#if SANITIZER_INTERCEPT_PVALLOC
+INTERCEPTOR(void*, pvalloc, SIZE_T size) {
+ ENSURE_COPYPROF_INITIALIZED();
+ return CopyProfPvalloc(size);
+}
+#endif
+
+#if SANITIZER_INTERCEPT_MALLOC_USABLE_SIZE
+INTERCEPTOR(SIZE_T, malloc_usable_size, void* ptr) {
+ ENSURE_COPYPROF_INITIALIZED();
+ return CopyProfMallocUsableSize(ptr);
+}
+#endif
+
+namespace __copyprof {
+
+void InitializeHeapInterceptors() {
+ COPYPROF_INTERCEPT_FUNC(malloc);
+ COPYPROF_INTERCEPT_FUNC(calloc);
+ COPYPROF_INTERCEPT_FUNC(realloc);
+ COPYPROF_INTERCEPT_FUNC(free);
+#if SANITIZER_INTERCEPT_CFREE
+ COPYPROF_INTERCEPT_FUNC(cfree);
+#endif
+#if SANITIZER_INTERCEPT_FREE_SIZED
+ COPYPROF_INTERCEPT_FUNC(free_sized);
+#endif
+#if SANITIZER_INTERCEPT_FREE_ALIGNED_SIZED
+ COPYPROF_INTERCEPT_FUNC(free_aligned_sized);
+#endif
+#if SANITIZER_INTERCEPT_REALLOCARRAY
+ COPYPROF_INTERCEPT_FUNC(reallocarray);
+#endif
+#if SANITIZER_INTERCEPT_MEMALIGN
+ COPYPROF_INTERCEPT_FUNC(memalign);
+#endif
+#if SANITIZER_INTERCEPT___LIBC_MEMALIGN
+ COPYPROF_INTERCEPT_FUNC(__libc_memalign);
+#endif
+#if SANITIZER_INTERCEPT_ALIGNED_ALLOC
+ COPYPROF_INTERCEPT_FUNC(aligned_alloc);
+#endif
+ COPYPROF_INTERCEPT_FUNC(posix_memalign);
+ COPYPROF_INTERCEPT_FUNC(valloc);
+#if SANITIZER_INTERCEPT_PVALLOC
+ COPYPROF_INTERCEPT_FUNC(pvalloc);
+#endif
+#if SANITIZER_INTERCEPT_MALLOC_USABLE_SIZE
+ COPYPROF_INTERCEPT_FUNC(malloc_usable_size);
+#endif
+}
+
+} // namespace __copyprof
diff --git a/compiler-rt/lib/copyprof/copyprof_state.h b/compiler-rt/lib/copyprof/copyprof_state.h
index b342f1de7a1c6b..e8b5f3bf2b0b4d 100644
--- a/compiler-rt/lib/copyprof/copyprof_state.h
+++ b/compiler-rt/lib/copyprof/copyprof_state.h
@@ -27,12 +27,14 @@ namespace __copyprof {
// 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, shadow memory is not updated (to avoid false negatives
-// during destruction), except that newly constructed objects are marked as
-// copy, so that destroying temporaries does not cause false negatives either
-// (see TemporaryObjectInDtorDoesNotSuppressReport). Note that newly created
-// temporary objects inside d'tors would still check their shadow memory when
-// destroyed, but if that is never updated when constructed, its state wouldn't
-// be meaningful. That's why shadow memory is marked as copy in d'tors as well.
+// during destruction), except that newly allocated or constructed memory is
+// marked as copy, so that freeing or destroying temporaries does not cause
+// false negatives either (see TemporaryObjectInDtorDoesNotSuppressReport). Note
+// that newly created temporary objects inside d'tors would still check their
+// shadow memory when destroyed, but if that is never updated when constructed,
+// its state wouldn't be meaningful. That's why shadow memory is marked as copy
+// in d'tors as well (see DISABLED_CopyModifiedByAnotherDtorIsNotReported for a
+// contrived example where this leads to FPs).
// 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 {
@@ -42,8 +44,8 @@ enum class SmfContext : u8 {
DTOR,
};
-// Whether memory that is newly constructed in `ctx` is marked as copy (see
-// `SmfContext`).
+// Whether memory that is newly allocated or constructed in `ctx` is marked as
+// copy (see `SmfContext`).
inline bool ShouldMarkNewMemoryAsCopy(SmfContext ctx) {
return ctx == SmfContext::COPY || ctx == SmfContext::DTOR;
}
diff --git a/compiler-rt/lib/copyprof/tests/copyprof_state_test.cpp b/compiler-rt/lib/copyprof/tests/copyprof_state_test.cpp
index c6eeb8fe7d9d6d..2ef72ebf44cebb 100644
--- a/compiler-rt/lib/copyprof/tests/copyprof_state_test.cpp
+++ b/compiler-rt/lib/copyprof/tests/copyprof_state_test.cpp
@@ -8,6 +8,7 @@
#include "copyprof/copyprof_state.h"
+#include "copyprof/copyprof_allocator.h"
#include "copyprof/copyprof_interface_internal.h"
#include "copyprof/copyprof_shadow.h"
#include "gtest/gtest.h"
@@ -207,5 +208,201 @@ TEST_F(CopyProfStateTest,
EXPECT_TRUE(__copyprof_state.is_transitive_copy);
}
+// Likewise for a temporary that a d'tor move-constructs, e.g. by moving a
+// member out (`std::string tmp = std::move(name_);`). Move c'tors are not
+// instrumented, so there is no c'tor callback. The same happens for c'tors from
+// an uninstrumented library, e.g. `std::string`'s copy c'tor from libstdc++ at
+// -O2 in C++17 mode.
+//
+// DISABLED: known limitation, see
+// TriviallyConstructedTemporaryInDtorDoesNotSuppressReport.
+TEST_F(CopyProfStateTest, DISABLED_MovedTemporaryInDtorDoesNotSuppressReport) {
+ char obj[32] = {0};
+ char source_obj[32] = {0};
+ char temp[32] = {0};
+
+ // `obj` is a copy that owns a buffer allocated by its copy c'tor.
+ __copyprof_copy_ctor_enter_callback(obj, source_obj, sizeof(obj));
+ void* buf = CopyProfMalloc(/*size=*/64);
+ __copyprof_copy_ctor_exit_callback(obj, source_obj, sizeof(obj));
+ ASSERT_NE(buf, nullptr);
+ // Memory occupied by `temp` is not a copy.
+ MarkApplicationMemory(temp, sizeof(temp), /*is_copy=*/false);
+
+ __copyprof_dtor_enter_callback(obj, sizeof(obj));
+ // Move `buf` from `obj` into `temp`. The move c'tor's stores are ignored in
+ // `DTOR` context, and there is no c'tor callback.
+ __copyprof_store_callback(temp, sizeof(temp));
+ __copyprof_store_callback(obj, sizeof(obj));
+ // Destroying `temp` frees `buf`.
+ __copyprof_dtor_enter_callback(temp, sizeof(temp));
+ CopyProfFree(buf);
+ __copyprof_dtor_exit_callback(temp, sizeof(temp));
+ __copyprof_dtor_exit_callback(obj, sizeof(obj));
+ EXPECT_TRUE(__copyprof_state.is_transitive_copy);
+}
+
+TEST_F(CopyProfStateTest,
+ DeallocatingUnmodifiedCopyAllocationPreservesTransitiveCopy) {
+ char obj[16] = {0};
+ char other[16] = {0};
+
+ __copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
+ void* copy_buf = CopyProfAllocate(/*size=*/32, /*align=*/8);
+ ASSERT_NE(copy_buf, nullptr);
+ EXPECT_TRUE(IsMarkedAsCopy(copy_buf, /*num_bytes=*/32));
+ __copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
+
+ __copyprof_dtor_enter_callback(obj, sizeof(obj));
+ CopyProfDeallocate(copy_buf, /*size=*/32, /*align=*/8);
+ EXPECT_TRUE(__copyprof_state.is_transitive_copy);
+ __copyprof_dtor_exit_callback(obj, sizeof(obj));
+}
+
+TEST_F(CopyProfStateTest,
+ DeallocatingModifiedCopyAllocationClearsTransitiveCopy) {
+ char obj[16] = {0};
+ char other[16] = {0};
+
+ __copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
+ void* modified_buf = CopyProfAllocate(/*size=*/32, /*align=*/8);
+ ASSERT_NE(modified_buf, nullptr);
+ __copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
+
+ __copyprof_store_callback(static_cast<char*>(modified_buf) + 8, /*size=*/1);
+ EXPECT_FALSE(IsMarkedAsCopy(modified_buf, /*num_bytes=*/32));
+
+ __copyprof_dtor_enter_callback(obj, sizeof(obj));
+ CopyProfDeallocate(modified_buf, /*size=*/32, /*align=*/8);
+ EXPECT_FALSE(__copyprof_state.is_transitive_copy);
+ __copyprof_dtor_exit_callback(obj, sizeof(obj));
+}
+
+TEST_F(CopyProfStateTest, MallocDuringCopyIsMarkedAsCopy) {
+ char obj[16] = {0};
+ char other[16] = {0};
+
+ __copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
+ void* buf = CopyProfMalloc(/*size=*/32);
+ __copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
+
+ ASSERT_NE(buf, nullptr);
+ EXPECT_TRUE(IsMarkedAsCopy(buf, /*num_bytes=*/32));
+ CopyProfFree(buf);
+}
+
+TEST_F(CopyProfStateTest, FreeingModifiedCopyAllocationClearsTransitiveCopy) {
+ char obj[16] = {0};
+ char other[16] = {0};
+
+ __copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
+ void* buf = CopyProfMalloc(/*size=*/32);
+ __copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
+ ASSERT_NE(buf, nullptr);
+ // Modifying `buf` marks it as non-copy.
+ __copyprof_store_callback(buf, /*size=*/1);
+
+ __copyprof_dtor_enter_callback(obj, sizeof(obj));
+ CopyProfFree(buf);
+ EXPECT_FALSE(__copyprof_state.is_transitive_copy);
+ __copyprof_dtor_exit_callback(obj, sizeof(obj));
+}
+
+// Freeing a temporary allocated by a d'tor must not clear `is_transitive_copy`,
+// regardless of the shadow state that the chunk's previous owner left behind.
+TEST_F(CopyProfStateTest, TemporaryAllocationInDtorPreservesTransitiveCopy) {
+ char obj[16] = {0};
+ char other[16] = {0};
+ // Free a non-copy chunk so that the temporary below likely reuses it.
+ CopyProfFree(CopyProfMalloc(/*size=*/32));
+
+ __copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
+ __copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
+
+ __copyprof_dtor_enter_callback(obj, sizeof(obj));
+ void* temp = CopyProfMalloc(/*size=*/32);
+ ASSERT_NE(temp, nullptr);
+ CopyProfFree(temp);
+ EXPECT_TRUE(__copyprof_state.is_transitive_copy);
+ __copyprof_dtor_exit_callback(obj, sizeof(obj));
+}
+
+// A d'tor (here `donor`'s) may allocate memory and hand it to another live copy
+// (here `holder`). The store into `holder` happens in `DTOR` context and is
+// ignored, and the memory is marked as copy, so `holder` looks unmodified when
+// it is destroyed and is reported as an unnecessary copy.
+//
+// DISABLED: accepted false positive. Marking memory allocated in `DTOR` context
+// as copy keeps temporaries from suppressing reports (see
+// TemporaryAllocationInDtorPreservesTransitiveCopy), which matters far more in
+// practice.
+TEST_F(CopyProfStateTest, DISABLED_CopyModifiedByAnotherDtorIsNotReported) {
+ char holder[32] = {0};
+ char source_holder[32] = {0};
+ char donor[16] = {0};
+
+ __copyprof_copy_ctor_enter_callback(holder, source_holder, sizeof(holder));
+ __copyprof_copy_ctor_exit_callback(holder, source_holder, sizeof(holder));
+
+ // `donor`'s d'tor allocates `extra` and stores a pointer to it in `holder`.
+ __copyprof_dtor_enter_callback(donor, sizeof(donor));
+ void* extra = CopyProfMalloc(/*size=*/16);
+ ASSERT_NE(extra, nullptr);
+ __copyprof_store_callback(holder, /*size=*/sizeof(extra));
+ __copyprof_dtor_exit_callback(donor, sizeof(donor));
+
+ // `holder`'s d'tor frees `extra`.
+ __copyprof_dtor_enter_callback(holder, sizeof(holder));
+ CopyProfFree(extra);
+ __copyprof_dtor_exit_callback(holder, sizeof(holder));
+ EXPECT_FALSE(__copyprof_state.is_transitive_copy);
+}
+
+TEST_F(CopyProfStateTest, CallocDuringCopyIsMarkedAsCopy) {
+ char obj[16] = {0};
+ char other[16] = {0};
+
+ __copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
+ void* buf = CopyProfCalloc(/*num=*/4, /*size=*/8);
+ __copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
+
+ ASSERT_NE(buf, nullptr);
+ EXPECT_TRUE(IsMarkedAsCopy(buf, /*num_bytes=*/32));
+ CopyProfFree(buf);
+}
+
+TEST_F(CopyProfStateTest, ReallocDuringCopyIsMarkedAsCopy) {
+ char obj[16] = {0};
+ char other[16] = {0};
+ void* buf = CopyProfMalloc(/*size=*/16);
+ ASSERT_NE(buf, nullptr);
+
+ __copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
+ void* resized_buf = CopyProfRealloc(buf, /*size=*/32);
+ __copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
+
+ ASSERT_NE(resized_buf, nullptr);
+ EXPECT_TRUE(IsMarkedAsCopy(resized_buf, /*num_bytes=*/32));
+ CopyProfFree(resized_buf);
+}
+
+// The shadow of reallocated memory follows the SMF context of the `realloc`
+// call; the shadow of the original memory is not carried over.
+TEST_F(CopyProfStateTest, ReallocOutsideCopyIsNotMarkedAsCopy) {
+ char obj[16] = {0};
+ char other[16] = {0};
+
+ __copyprof_copy_ctor_enter_callback(obj, other, sizeof(obj));
+ void* buf = CopyProfMalloc(/*size=*/16);
+ __copyprof_copy_ctor_exit_callback(obj, other, sizeof(obj));
+ ASSERT_NE(buf, nullptr);
+ ASSERT_TRUE(IsMarkedAsCopy(buf, /*num_bytes=*/16));
+
+ void* resized_buf = CopyProfRealloc(buf, /*size=*/32);
+ ASSERT_NE(resized_buf, nullptr);
+ EXPECT_FALSE(IsMarkedAsCopy(resized_buf, /*num_bytes=*/32));
+ CopyProfFree(resized_buf);
+}
+
} // namespace
} // namespace __copyprof
More information about the llvm-branch-commits
mailing list