-
-
Notifications
You must be signed in to change notification settings - Fork 907
Description
Labels Suggestion
bugmemory-leakcritical
Issue Body
Description
TracyVector has a critical memory leak when used with non-trivially-destructible types. The destructor only calls free() on the internal buffer without invoking element destructors, causing resource leaks in nested containers and types with manual resource management.
TracyVector is a fundamental data structure in Tracy, widely used throughout the codebase:
Usage Statistics in Tracy Server
Total Vector usage: 25+ distinct uses in tracy/server/TracyEvent.hpp alone
Critical nested Vector usage (from TracyWorker.hpp):
// Line ~500: Zone event hierarchy
Vector<Vector<short_ptr<ZoneEvent>>> zoneChildren;
// Line ~520: GPU event hierarchy
Vector<Vector<short_ptr<GpuEvent>>> gpuChildren;
// Line ~540: Ghost zone tracking
Vector<Vector<GhostZone>> ghostChildren;
// Line ~560: Zone vector cache
Vector<Vector<short_ptr<ZoneEvent>>> zoneVectorCache;Other common uses:
- Timeline storage:
Vector<short_ptr<ZoneEvent>> timeline - Stack tracking:
Vector<short_ptr<ZoneEvent>> stack - Message queues:
Vector<short_ptr<MessageData>> messages - Sample data:
Vector<SampleData> samples - Memory events:
Vector<MemEvent> data - Frame tracking:
Vector<FrameEvent> frames - Lock timelines:
Vector<LockEventPtr> timeline
Impact: The bug affects all non-trivially-destructible types stored in Vector, with nested Vectors being the most critical case as they leak inner buffers.
Affected Code
File: tracy/server/TracyVector.hpp
Buggy locations:
- Destructor
~Vector()(line ~250) - Move assignment
operator=(Vector&&)(line ~91) clear()function (line ~110)Realloc()function (line ~305)
Minimal Reproduction
#include "tracy/server/TracyVector.hpp"
#include <cstdio>
struct ResourceOwner {
int* data_;
ResourceOwner(int val) : data_(new int(val)) {
printf("Allocated %p\n", (void*)data_);
}
~ResourceOwner() {
printf("Freeing %p\n", (void*)data_);
delete data_; // Never called with buggy TracyVector!
}
};
int main() {
tracy::Vector<ResourceOwner> vec;
vec.push_back(ResourceOwner(1));
vec.push_back(ResourceOwner(2));
// Destructors never called → memory leak!
return 0;
}AddressSanitizer Output:
Direct leak of 4 byte(s) in 1 object(s) allocated from:
#0 in operator new(unsigned long)
#1 in ResourceOwner::ResourceOwner(int)
Direct leak of 4 byte(s) in 1 object(s) allocated from:
#0 in operator new(unsigned long)
#1 in ResourceOwner::ResourceOwner(int)
SUMMARY: AddressSanitizer: 8 byte(s) leaked in 2 allocation(s).
Real-World Impact: Nested Vectors
Tracy's actual production code (TracyWorker.hpp:500):
Vector<Vector<short_ptr<ZoneEvent>>> zoneChildren;This demonstrates a nested container scenario. With the buggy implementation:
tracy::Vector<tracy::Vector<int>> outer;
tracy::Vector<int> inner1;
inner1.push_back(1);
inner1.push_back(2);
outer.push_back(std::move(inner1));
// When outer destructs:
// 1. free(outer.m_ptr) is called
// 2. BUT inner Vector destructors are NEVER called!
// 3. Result: inner1.m_ptr memory is leakedAddressSanitizer confirms:
Direct leak of 8 byte(s) in 1 object(s) allocated from:
#1 in tracy::Vector<int>::Realloc() TracyVector.hpp:308
#4 in test_nested_vector()
SUMMARY: AddressSanitizer: 16 byte(s) leaked (nested Vector buffers)
Root Cause Analysis
Current implementation (buggy):
~Vector() {
if (m_ptr) {
free(m_ptr); // ❌ Only frees memory, doesn't call m_ptr[i].~T()
}
}Why this fails:
Vector<int>is NOT trivially destructible (has user-defined destructor)- When
Vector<Vector<T>>destructs, outerfree()doesn't call inner destructors - Inner
m_ptrbuffers are never freed → memory leak
Type traits check:
static_assert(!std::is_trivially_destructible_v<tracy::Vector<int>>,
"Vector has user-defined destructor!");Proposed Fix
Use if constexpr with type traits to call destructors only when needed:
~Vector() {
if (m_ptr) {
if constexpr (!std::is_trivially_destructible_v<T>) {
for (uint32_t i = 0; i < m_size; i++) {
m_ptr[i].~T(); // ✅ Explicitly call destructors
}
}
free(m_ptr);
}
}Same pattern applies to:
- Move assignment operator
clear()functionRealloc()function
PoC: How to Reproduce Using Provided Test Suite
Step 1: save the test file to test_before_after_fix.cpp
/**
* @file test_before_after_fix.cpp
* @brief Before/After Fix Comparison Test - Memory Leak Detection with AddressSanitizer
*
* Compilation Method 1 - Test Original Version (with leaks):
* cd /Test/c-cpp-container-optimizer/benchmarks
* g++ -std=c++17 -fsanitize=address -fno-omit-frame-pointer -g -I. \
* test_before_after_fix.cpp tracy/server/TracyMemory.cpp -o test_original
* ./test_original
*
* Compilation Method 2 - Test Fixed Version (no leaks):
* cd /Test/c-cpp-container-optimizer/benchmarks
* g++ -std=c++17 -fsanitize=address -fno-omit-frame-pointer -g -I. \
* -DUSE_FIXED_VERSION \
* test_before_after_fix.cpp tracy/server/TracyMemory.cpp -o test_fixed
* ./test_fixed
*/
#include <iostream>
#ifdef USE_FIXED_VERSION
// For fixed version, also include the fixed version and use it
#include "tracy/server/TracyVector.hpp"
template<typename T>
using VectorImpl = tracy::Vector<T>;
#define VERSION_NAME "Fixed Version (TracyVector.hpp)"
#define VERSION_TAG "FIXED"
#else
// Include Tracy Vector
// #include "tracy/server/TracyVector_original.hpp"
#include "tracy/server/TracyVector.hpp"
// Test original version
template<typename T>
using VectorImpl = tracy::Vector<T>;
#define VERSION_NAME "Original Version (TracyVector.hpp)"
#define VERSION_TAG "ORIGINAL"
#endif
// Include Tracy's actual types
#include "tracy/server/TracyEvent.hpp"
// Test non-trivial type with resource management
class ResourceOwner {
public:
int* data_;
int id_;
ResourceOwner(int val) : data_(new int(val)), id_(val) {
std::cout << " [ResourceOwner " << id_ << "] Constructor, allocated: " << data_ << "\n";
}
~ResourceOwner() {
if (data_) {
std::cout << " [ResourceOwner " << id_ << "] Destructor, deleting: " << data_ << "\n";
delete data_;
} else {
std::cout << " [ResourceOwner " << id_ << "] Destructor (moved-from)\n";
}
}
ResourceOwner(const ResourceOwner& other)
: data_(new int(*other.data_)), id_(other.id_) {
std::cout << " [ResourceOwner " << id_ << "] Copy constructor\n";
}
ResourceOwner(ResourceOwner&& other) noexcept
: data_(other.data_), id_(other.id_) {
other.data_ = nullptr;
std::cout << " [ResourceOwner " << id_ << "] Move constructor\n";
}
ResourceOwner& operator=(ResourceOwner&& other) noexcept {
if (this != &other) {
delete data_;
data_ = other.data_;
id_ = other.id_;
other.data_ = nullptr;
}
return *this;
}
};
// POD type
struct PodType {
int id;
double value;
};
// ============================================================================
// Test Functions
// ============================================================================
void test_non_trivial_type() {
std::cout << "\n--- TEST 1: Non-Trivial Type (ResourceOwner) ---\n";
std::cout << "Creating Vector<ResourceOwner>...\n\n";
{
VectorImpl<ResourceOwner> vec;
std::cout << "Adding ResourceOwner(1):\n";
vec.push_back(ResourceOwner(1));
std::cout << "\nAdding ResourceOwner(2):\n";
vec.push_back(ResourceOwner(2));
std::cout << "\nVector size: " << vec.size() << "\n\n";
std::cout << "--- Vector about to destruct ---\n";
}
std::cout << "--- Vector destruction completed ---\n\n";
}
void test_pod_type() {
std::cout << "\n--- TEST 2: POD Type (PodType) ---\n";
std::cout << "Creating Vector<PodType>...\n\n";
{
VectorImpl<PodType> vec;
vec.push_back(PodType{1, 1.5});
vec.push_back(PodType{2, 2.5});
std::cout << " Added 2 elements, size=" << vec.size() << "\n";
}
std::cout << " ✅ POD type test completed\n\n";
}
void test_nested_vector() {
std::cout << "\n--- TEST 3: Nested Vector (Vector<Vector<int>>) ---\n";
std::cout << "This is a critical test!\n\n";
{
VectorImpl<VectorImpl<int>> nested;
VectorImpl<int> inner1;
inner1.push_back(1);
inner1.push_back(2);
nested.push_back(std::move(inner1));
VectorImpl<int> inner2;
inner2.push_back(3);
inner2.push_back(4);
nested.push_back(std::move(inner2));
std::cout << " Outer Vector size: " << nested.size() << "\n";
std::cout << " Inner[0] size: " << nested[0].size() << "\n";
std::cout << " Inner[1] size: " << nested[1].size() << "\n\n";
std::cout << "--- Nested Vector about to destruct ---\n";
}
std::cout << "--- Nested Vector destruction completed ---\n\n";
}
void test_tracy_symbol_location() {
std::cout << "\n--- TEST 4: Tracy Actual Type (Vector<SymbolLocation>) ---\n";
std::cout << "Testing Tracy's real SymbolLocation struct...\n\n";
{
VectorImpl<tracy::SymbolLocation> vec;
tracy::SymbolLocation loc1{0x1000, 100};
tracy::SymbolLocation loc2{0x2000, 200};
vec.push_back(loc1);
vec.push_back(loc2);
std::cout << " Added 2 SymbolLocations, size=" << vec.size() << "\n";
std::cout << " [0]: addr=0x" << std::hex << vec[0].addr << std::dec << ", len=" << vec[0].len << "\n";
std::cout << " [1]: addr=0x" << std::hex << vec[1].addr << std::dec << ", len=" << vec[1].len << "\n\n";
}
std::cout << " ✅ Tracy SymbolLocation test completed\n\n";
}
void test_tracy_zone_event() {
std::cout << "\n--- TEST 5: Tracy ZoneEvent (Vector<short_ptr<ZoneEvent>>) ---\n";
std::cout << "Testing Tracy's ZoneEvent with short_ptr...\n\n";
{
VectorImpl<tracy::short_ptr<tracy::ZoneEvent>> vec;
// Create ZoneEvents on heap (as Tracy does)
auto* event1 = new tracy::ZoneEvent();
auto* event2 = new tracy::ZoneEvent();
event1->SetStartSrcLoc(1000, 1);
event2->SetStartSrcLoc(2000, 2);
tracy::short_ptr<tracy::ZoneEvent> ptr1(event1);
tracy::short_ptr<tracy::ZoneEvent> ptr2(event2);
vec.push_back(ptr1);
vec.push_back(ptr2);
std::cout << " Added 2 ZoneEvent pointers, size=" << vec.size() << "\n";
std::cout << " [0]: Start=" << vec[0]->Start() << ", SrcLoc=" << vec[0]->SrcLoc() << "\n";
std::cout << " [1]: Start=" << vec[1]->Start() << ", SrcLoc=" << vec[1]->SrcLoc() << "\n\n";
// Manual cleanup for this test
delete event1;
delete event2;
}
std::cout << " ✅ Tracy ZoneEvent test completed\n\n";
}
void test_tracy_nested_zone_children() {
std::cout << "\n--- TEST 6: CRITICAL - Tracy's Actual Usage: Vector<Vector<short_ptr<ZoneEvent>>> ---\n";
std::cout << "This mirrors the real bug in TracyWorker.hpp: zoneChildren field!\n\n";
{
// This is the exact type used in Tracy's Worker class
VectorImpl<VectorImpl<tracy::short_ptr<tracy::ZoneEvent>>> zoneChildren;
// Create first child vector
VectorImpl<tracy::short_ptr<tracy::ZoneEvent>> children1;
auto* child1 = new tracy::ZoneEvent();
child1->SetStartSrcLoc(100, 1);
children1.push_back(tracy::short_ptr<tracy::ZoneEvent>(child1));
// Create second child vector
VectorImpl<tracy::short_ptr<tracy::ZoneEvent>> children2;
auto* child2 = new tracy::ZoneEvent();
child2->SetStartSrcLoc(200, 2);
children2.push_back(tracy::short_ptr<tracy::ZoneEvent>(child2));
zoneChildren.push_back(std::move(children1));
zoneChildren.push_back(std::move(children2));
std::cout << " Outer Vector size: " << zoneChildren.size() << "\n";
std::cout << " Inner[0] size: " << zoneChildren[0].size() << "\n";
std::cout << " Inner[1] size: " << zoneChildren[1].size() << "\n\n";
std::cout << "--- Nested zoneChildren Vector about to destruct ---\n";
// Manual cleanup
delete child1;
delete child2;
}
std::cout << "--- Nested zoneChildren Vector destruction completed ---\n\n";
}
// ============================================================================
// Main Program
// ============================================================================
int main() {
std::cout << "\n";
std::cout << "╔════════════════════════════════════════════════════════════════╗\n";
std::cout << "║ ║\n";
std::cout << "║ Tracy Vector - AddressSanitizer Memory Leak Test ║\n";
std::cout << "║ ║\n";
std::cout << "╚════════════════════════════════════════════════════════════════╝\n\n";
std::cout << "Current Test Version: " << VERSION_NAME << "\n";
std::cout << "Version Tag: " << VERSION_TAG << "\n\n";
std::cout << "════════════════════════════════════════════════════════════════\n";
#ifdef USE_FIXED_VERSION
std::cout << "\nFix Description:\n";
std::cout << " ✅ Smart destruction with if constexpr\n";
std::cout << " ✅ Non-trivial types call element destructors\n";
std::cout << " ✅ POD types have zero overhead (compile-time optimization)\n\n";
std::cout << "Code Example:\n";
std::cout << " ~Vector() {\n";
std::cout << " if (m_ptr) {\n";
std::cout << " if constexpr (!std::is_trivially_destructible_v<T>) {\n";
std::cout << " for (uint32_t i = 0; i < m_size; i++) {\n";
std::cout << " m_ptr[i].~T(); // ✅ Call element destructors\n";
std::cout << " }\n";
std::cout << " }\n";
std::cout << " free(m_ptr);\n";
std::cout << " }\n";
std::cout << " }\n";
#else
std::cout << "\nOriginal Implementation Problems:\n";
std::cout << " ❌ Does not call element destructors\n";
std::cout << " ❌ Non-trivial types will leak memory\n";
std::cout << " ❌ Vector<Vector<T>> will leak inner Vectors\n\n";
std::cout << "Original Code:\n";
std::cout << " ~Vector() {\n";
std::cout << " if (m_ptr) {\n";
std::cout << " free(m_ptr); // ❌ Does not call m_ptr[i].~T()\n";
std::cout << " }\n";
std::cout << " }\n";
#endif
std::cout << "\n════════════════════════════════════════════════════════════════\n";
std::cout << "Running tests...\n";
std::cout << "════════════════════════════════════════════════════════════════\n";
test_non_trivial_type();
test_pod_type();
test_nested_vector();
test_tracy_symbol_location();
test_tracy_zone_event();
test_tracy_nested_zone_children();
std::cout << "\n════════════════════════════════════════════════════════════════\n";
std::cout << "Test Completed\n";
std::cout << "════════════════════════════════════════════════════════════════\n\n";
#ifdef USE_FIXED_VERSION
std::cout << "✅ Expected Result: AddressSanitizer should NOT report memory leaks\n";
std::cout << " All element destructors are called correctly\n\n";
std::cout << "How it works:\n";
std::cout << " - if constexpr checks if T needs destructor at compile time\n";
std::cout << " - For non-trivial types: calls m_ptr[i].~T() for each element\n";
std::cout << " - For POD types: skips destructor calls (zero overhead)\n\n";
#else
std::cout << "⚠️ Expected Result: AddressSanitizer WILL report memory leaks\n\n";
std::cout << "Expected leak report breakdown:\n";
std::cout << " Leak #1-2: TEST 3 - Nested Vector<Vector<int>>\n";
std::cout << " - Direct leak of 8 byte(s): inner1.m_ptr (2 ints × 4 bytes)\n";
std::cout << " - Direct leak of 8 byte(s): inner2.m_ptr (2 ints × 4 bytes)\n";
std::cout << " - Source: Vector<int>::Realloc() at TracyVector.hpp:308\n";
std::cout << " - Why: Outer Vector doesn't call inner Vector destructors\n\n";
std::cout << " Leak #3-4: TEST 6 - Tracy's Vector<Vector<short_ptr<ZoneEvent>>>\n";
std::cout << " - Direct leak of 6 byte(s): children1.m_ptr (1 short_ptr × 6 bytes)\n";
std::cout << " - Direct leak of 6 byte(s): children2.m_ptr (1 short_ptr × 6 bytes)\n";
std::cout << " - Source: Vector<short_ptr<ZoneEvent>>::AllocMore()\n";
std::cout << " - Why: Same bug - outer Vector doesn't call inner destructors\n";
std::cout << " - ⚠️ This is Tracy's ACTUAL production code bug!\n\n";
std::cout << " Leak #5-6: TEST 1 - ResourceOwner\n";
std::cout << " - Direct leak of 4 byte(s): ResourceOwner(1).data_ (new int)\n";
std::cout << " - Direct leak of 4 byte(s): ResourceOwner(2).data_ (new int)\n";
std::cout << " - Source: operator new in ResourceOwner::ResourceOwner()\n";
std::cout << " - Why: Vector doesn't call ~ResourceOwner(), so data_ never deleted\n\n";
std::cout << " TOTAL: 36 bytes leaked in 6 allocations\n";
std::cout << " = 16 bytes (nested Vector m_ptr)\n";
std::cout << " + 12 bytes (Tracy zoneChildren m_ptr)\n";
std::cout << " + 8 bytes (ResourceOwner data_)\n\n";
std::cout << "Stack traces will show:\n";
std::cout << " - Allocation call chain (bottom to top)\n";
std::cout << " - Exact line numbers where memory was allocated\n";
std::cout << " - Function names in the call stack\n\n";
#endif
return 0;
}
Adjust tracy/server/TracySort.hpp to enable PoC to include the related types.
#ifndef __TRACYSORT_HPP__
#define __TRACYSORT_HPP__
// Use tracy_pdqsort.h instead of ppqsort.h to avoid external dependency
#include "tracy_pdqsort.h"
// Create compatibility layer for ppqsort API using tracy pdqsort
namespace ppqsort {
namespace execution {
struct par_t {};
static constexpr par_t par{};
}
template<typename Execution, typename Iter, typename Compare>
inline void sort(Execution, Iter begin, Iter end, Compare comp) {
// Delegate to tracy::pdqsort
tracy::pdqsort_branchless(begin, end, comp);
}
}
#endif
Step 2: Compile with ORIGINAL (buggy) TracyVector
g++ -std=c++17 -fsanitize=address -fno-omit-frame-pointer -g \
-I. test_before_after_fix.cpp tracy/server/TracyMemory.cpp \
-DTRACY_ENABLE -o test_original
# Run and observe leaks
./test_original 2>&1 | tail -50Expected output (BEFORE fix):
==XXXXX==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 8 byte(s) in 1 object(s) allocated from:
#1 in tracy::Vector<int>::Realloc() TracyVector.hpp:308
#4 in test_nested_vector() test_before_after_fix.cpp:134
[... 5 more leaks ...]
SUMMARY: AddressSanitizer: 36 byte(s) leaked in 6 allocation(s).
Step 3: Apply the fix and recompile
# Rename TracyVector.hpp to backup
mv tracy/server/TracyVector.hpp tracy/server/TracyVector_original.hpp
# Apply fixed version (from PR)
cp TracyVector_Fixed.hpp tracy/server/TracyVector.hpp
# Recompile
g++ -std=c++17 -fsanitize=address -fno-omit-frame-pointer -g \
-I. test_before_after_fix.cpp tracy/server/TracyMemory.cpp \
-DTRACY_ENABLE -o test_fixed
# Run and verify NO leaks
./test_fixed 2>&1 | tail -20Expected output (AFTER fix):
════════════════════════════════════════════════════════════════
Test Completed
════════════════════════════════════════════════════════════════
Expected Result: AddressSanitizer should NOT report memory leaks
All element destructors are called correctly
(clean exit with code 0 - no leak reports)
Step 4: Compare results
The test clearly demonstrates:
- Before: 36 bytes leaked (inner Vector/ResourceOwner buffers never freed)
- After: 0 bytes leaked (all destructors properly called)
Environment
- Tracy version: Latest master branch (tested on commit from Nov 2025)
- Compiler: GCC 11+ with
-std=c++17 - Detection tool: AddressSanitizer/LeakSanitizer (
-fsanitize=address)