Memory Technologies Development Only LSan - antimetal/system-agent GitHub Wiki

LeakSanitizer (LSAN)

Overview

LeakSanitizer (LSAN) is a dedicated memory leak detector that is part of the LLVM sanitizer suite. It can be used standalone or integrated with AddressSanitizer (ASAN) to provide comprehensive memory error and leak detection. LSAN is specifically designed as a run-time memory leak detector that performs leak detection at program termination.

Key Characteristics:

  • Dedicated memory leak detector from LLVM sanitizer suite
  • Can run standalone with -fsanitize=leak or with ASAN
  • Minimal overhead during program execution (detection happens at exit)
  • Requires recompilation with appropriate sanitizer flags
  • Part of the broader Google sanitizers project

Performance Characteristics

Metric Value Notes
Runtime Overhead Minimal (~0%) LSan lies dormant during execution
Exit Overhead Moderate Stop-the-world leak checking at program termination
Memory Overhead Low-Moderate Tracks allocation metadata
Accuracy High Definitive leak detection with reachability analysis
False Positives Very Low Uses conservative reachability analysis
Production Ready No Development and testing only
Platform Support Linux, macOS, Android, Fuchsia, NetBSD Best support on x86_64 Linux

Performance Notes:

  • LSAN adds almost no performance overhead during normal program execution
  • The tool "lies dormant" until process termination
  • All overhead occurs during the final leak detection phase
  • When used with ASAN, no additional slowdown compared to plain ASAN
  • Standalone mode avoids ASAN instrumentation overhead for performance-critical scenarios

How It Works

LSAN employs a sophisticated approach to detect memory leaks through reachability analysis:

Allocation Tracking

  • Intercepts all memory allocation calls (malloc, new, etc.)
  • Maintains metadata about each allocation
  • Tracks allocation size, location, and call stack

Reachability Analysis

  • At program termination, performs stop-the-world analysis
  • Scans all reachable memory regions (stack, globals, heap)
  • Identifies memory that cannot be reached from any live pointer
  • Uses conservative pointer scanning (treats any aligned value as potential pointer)

Leak Classification

  • Direct leaks: Unreachable allocations
  • Indirect leaks: Allocations only reachable through direct leaks
  • Possible leaks: Memory that may be leaked (ambiguous pointers)

Detection Process

  1. Program runs normally with minimal instrumentation
  2. At exit, LSAN suspends all threads
  3. Scans memory regions for pointers to allocated blocks
  4. Builds reachability graph
  5. Reports unreachable allocations as leaks
  6. Provides stack traces for leak origins

System-Agent Implementation Plan

LSAN is designed for development and testing environments, not production systems:

Development Environment Integration

# Development builds with leak detection
export CC=clang
export CXX=clang++
export CFLAGS="-fsanitize=leak -g"
export CXXFLAGS="-fsanitize=leak -g"

# Build with leak detection
make clean && make

CI/CD Pipeline Integration

# GitHub Actions example
- name: Build with LeakSanitizer
  run: |
    export CC=clang
    export CXX=clang++
    cmake -DCMAKE_C_FLAGS="-fsanitize=leak -g" \
          -DCMAKE_CXX_FLAGS="-fsanitize=leak -g" \
          .
    make -j$(nproc)

- name: Run leak detection tests
  run: |
    export LSAN_OPTIONS="suppressions=.lsan-suppressions:print_suppressions=1"
    ctest --output-on-failure

Pre-release Testing Strategy

  • Integration with existing test suites
  • Automated leak detection in staging environments
  • Performance regression testing
  • Memory usage baseline establishment

Production Exclusion

Never deploy LSAN in production:

  • Security considerations - not hardened for production
  • Performance impact at program termination
  • Potential for process hanging during leak detection
  • Debug symbols and metadata overhead

Compilation

Standalone LSAN

# Basic leak detection only
clang -fsanitize=leak -g program.c -o program

# With debugging information
clang -fsanitize=leak -g -O0 -fno-omit-frame-pointer program.c -o program

# C++ programs
clang++ -fsanitize=leak -g -std=c++17 program.cpp -o program

With AddressSanitizer

# Combined ASAN + LSAN (LSAN automatically enabled)
clang -fsanitize=address -g program.c -o program

# Explicitly enabling leak detection with ASAN
clang -fsanitize=address -g program.c -o program
# Run with: ASAN_OPTIONS=detect_leaks=1 ./program

Link Requirements

  • Must use clang/clang++ for linking (not ld directly)
  • Sanitizer runtime library automatically linked
  • Ensure proper symbolication with -g flag
  • Frame pointers recommended: -fno-omit-frame-pointer

Static vs Dynamic Linking

# Dynamic linking (default)
clang -fsanitize=leak program.c

# Static linking (if needed)
clang -fsanitize=leak -static-libsan program.c

Cross-compilation Considerations

  • Sanitizer runtime must be available for target platform
  • May require building LLVM compiler-rt for target
  • Limited platform support compared to native builds

Features

Core Leak Detection

  • Exit-time detection: Automatic leak checking at program termination
  • On-demand detection: Programmatic leak checking via __lsan_do_recoverable_leak_check()
  • Periodic checking: Can be configured for interval-based detection
  • Fine-grained control: Enable/disable detection dynamically

Leak Reporting

// Example leak report
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 7 byte(s) in 1 object(s) allocated from:
    #0 0x4af01b in __interceptor_malloc /lib/asan_malloc_linux.cc:52:3
    #1 0x4da26a in main program.c:10:7
    #2 0x7f076fd9cec4 in __libc_start_main libc-start.c:287

SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s).

Suppression Support

  • File-based suppression rules
  • Pattern matching against stack traces
  • Function, file, and library name matching
  • Hierarchical suppression rules

Integration APIs

#include <sanitizer/lsan_interface.h>

// Programmatic leak detection
void __lsan_do_recoverable_leak_check();

// Disable leak detection for specific regions
void __lsan_disable();
void __lsan_enable();

// Register/unregister root regions
void __lsan_register_root_region(const void *p, size_t size);
void __lsan_unregister_root_region(const void *p, size_t size);

// Ignore specific allocations
void __lsan_ignore_object(const void *p);

Code Examples

Basic Leak Detection Setup

// memory-leak.c
#include <stdlib.h>
#include <stdio.h>

void *global_ptr;

int main() {
    // This allocation will be leaked
    global_ptr = malloc(100);
    global_ptr = NULL; // Pointer lost, memory leaked
    
    // This allocation is reachable and won't be reported
    void *reachable = malloc(50);
    
    printf("Program ending - LSAN will check for leaks\n");
    
    // Cleanup reachable allocation
    free(reachable);
    
    return 0; // LSAN leak detection triggers here
}

Compilation and Execution

# Compile with LSAN
clang -fsanitize=leak -g memory-leak.c -o memory-leak

# Run with default options
./memory-leak

# Run with custom options
LSAN_OPTIONS="print_suppressions=1:report_objects=1" ./memory-leak

Suppression File Example

# .lsan-suppressions
# Suppress known third-party library leaks
leak:libcrypto.so
leak:libssl.so

# Suppress specific function leaks
leak:known_leaky_function

# Suppress by source file
leak:third_party_code.c

# Pattern matching with wildcards
leak:*_create_context

CI Integration Example

#!/bin/bash
# ci-leak-test.sh

set -e

# Build with leak detection
export CC=clang
export CFLAGS="-fsanitize=leak -g -O0"
make clean && make

# Configure LSAN options
export LSAN_OPTIONS="suppressions=.lsan-suppressions:print_suppressions=1:exitcode=1"

# Run test suite
echo "Running tests with leak detection..."
make test

# Run specific leak-prone components
echo "Testing memory management modules..."
./test_allocator
./test_cache_manager
./test_connection_pool

echo "Leak detection tests completed successfully"

Advanced Usage with ASAN

// advanced-example.c
#include <stdlib.h>
#include <sanitizer/lsan_interface.h>

void controlled_leak_test() {
    // Temporarily disable leak detection
    __lsan_disable();
    
    void *intentional_leak = malloc(64);
    // This won't be reported as a leak
    
    __lsan_enable();
    
    void *tracked_allocation = malloc(32);
    free(tracked_allocation); // Properly cleaned up
}

int main() {
    controlled_leak_test();
    
    // Force leak check before exit
    __lsan_do_recoverable_leak_check();
    
    return 0;
}

Runtime Options

LSAN_OPTIONS Environment Variable

Configure LSAN behavior through the LSAN_OPTIONS environment variable:

# Basic configuration
export LSAN_OPTIONS="print_suppressions=1:report_objects=1"

# Comprehensive configuration
export LSAN_OPTIONS="suppressions=.lsan-suppressions:print_suppressions=1:report_objects=1:max_leaks=10:exitcode=1"

Core Options

Option Default Description
exitcode 23 Exit code when leaks detected
max_leaks 0 Maximum number of leaks to report (0 = unlimited)
suppressions (none) Path to suppression rules file
print_suppressions 1 Print suppression statistics
report_objects 0 Report addresses of leaked objects
use_unaligned 0 Consider unaligned pointers during scanning
detect_leaks 1 Enable/disable leak detection (standalone mode)

Verbosity and Output Control

# Verbose output with object addresses
LSAN_OPTIONS="report_objects=1:print_suppressions=1"

# Limit leak reports
LSAN_OPTIONS="max_leaks=5"

# Custom exit code for CI integration
LSAN_OPTIONS="exitcode=42"

Stack Trace Configuration

Inherited from ASAN flags when used together:

# Improve stack trace quality
ASAN_OPTIONS="fast_unwind_on_malloc=0:malloc_context_size=30:strip_path_prefix=/usr/src/"

# For standalone LSAN
LSAN_OPTIONS="fast_unwind_on_malloc=0:malloc_context_size=30"

Symbolization Options

# Custom symbolizer path
export LSAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer

# Symbolization options
LSAN_OPTIONS="symbolize=1:print_module_map=1"

Platform-Specific Options

# Linux-specific
LSAN_OPTIONS="use_tls=1:use_ld_allocations=1"

# macOS-specific (when supported)
ASAN_OPTIONS="detect_leaks=1"  # Enable on macOS

Output Interpretation

Leak Report Structure

=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 128 byte(s) in 1 object(s) allocated from:
    #0 0x7f8b8c0a5b40 in __interceptor_malloc /llvm/compiler-rt/lib/asan/asan_malloc_linux.cc:145
    #1 0x400a3c in leaky_function /home/user/program.c:15:12
    #2 0x400a8f in main /home/user/program.c:25:5
    #3 0x7f8b8b8c0830 in __libc_start_main /lib/x86_64-linux-gnu/libc-start.c:291

Indirect leak of 64 byte(s) in 2 object(s) allocated from:
    #0 0x7f8b8c0a5b40 in __interceptor_malloc /llvm/compiler-rt/lib/asan/asan_malloc_linux.cc:145
    #1 0x400a55 in create_node /home/user/program.c:8:20
    #2 0x400a3c in leaky_function /home/user/program.c:16:8
    #3 0x400a8f in main /home/user/program.c:25:5

SUMMARY: AddressSanitizer: 192 byte(s) leaked in 3 allocation(s).

Leak Types Explained

Direct Leaks

  • Memory allocations that are completely unreachable
  • No pointer to the allocation exists in reachable memory
  • These are definite memory leaks
  • Should be fixed by proper cleanup or resource management

Indirect Leaks

  • Memory allocations only reachable through direct leaks
  • Example: nodes in a leaked data structure
  • Often consequence of direct leaks
  • Fixing direct leaks typically resolves indirect leaks

Possible Leaks

  • Memory that may or may not be leaked
  • Conservative pointer analysis found ambiguous references
  • Could be legitimate memory usage or actual leaks
  • Requires manual inspection to determine

Stack Trace Analysis

#0 0x7f8b8c0a5b40 in __interceptor_malloc    # LSAN malloc interceptor
#1 0x400a3c in leaky_function               # Your function that allocated
#2 0x400a8f in main                         # Call chain leading to allocation

Key Information:

  • Frame #0: Always shows the intercepted allocation function
  • Frame #1: Your code that performed the allocation
  • Subsequent frames: Call chain leading to the allocation
  • Addresses: Can be resolved to source locations with debug symbols

Suppression Matching Output

-----------------------------------------------------
Suppressions used:
  count      bytes template
      1         64 leak:third_party_lib
      2        128 leak:*_create_*
-----------------------------------------------------

Interpretation:

  • Shows which suppression rules matched
  • Count: Number of leaks suppressed by each rule
  • Bytes: Total memory suppressed by each rule
  • Template: The suppression pattern that matched

Exit Codes and Return Values

Exit Code Meaning
0 No leaks detected
23 Leaks detected (default)
Custom As configured by exitcode option
# Check for leaks in scripts
./program
if [ $? -eq 23 ]; then
    echo "Memory leaks detected!"
    exit 1
fi

Suppressions

Suppression File Format

Suppressions allow you to ignore known or acceptable leaks. Create a text file with one suppression rule per line:

# .lsan-suppressions
# Format: leak:<pattern>

# Suppress specific function
leak:known_leaky_function

# Suppress by source file  
leak:third_party_library.c

# Suppress by binary/library
leak:libcrypto.so.1.1

# Pattern matching with wildcards
leak:*_alloc_*
leak:SSL_*

# Suppress by full path
leak:/usr/lib/x86_64-linux-gnu/libssl.so*

Pattern Matching Rules

  • Substring matching: Pattern matches any part of the stack trace
  • Wildcards supported: * matches any sequence of characters
  • Special anchors: ^ (beginning) and $ (end) for exact matching
  • Case sensitive: Patterns are case-sensitive
  • Multiple matches: Any frame in stack trace can trigger suppression

Advanced Suppression Examples

# Suppress specific allocation sites
leak:^malloc_wrapper$
leak:allocate_buffer

# Suppress by source location
leak:memory_pool.cpp:45
leak:*/third_party/*

# Suppress by function signature
leak:std::__1::vector<*>::reserve
leak:operator new*

# Suppress by library
leak:libpthread.so*
leak:libc.so*
leak:libstdc++.so*

# Suppress OpenSSL known issues
leak:OPENSSL_malloc
leak:CRYPTO_malloc
leak:SSL_CTX_new

Common Third-Party Library Suppressions

# OpenSSL
leak:OPENSSL_malloc
leak:CRYPTO_malloc
leak:SSL_*
leak:libssl.so*
leak:libcrypto.so*

# Python runtime
leak:PyMem_Malloc
leak:_PyObject_New
leak:libpython*.so*

# GLib/GTK
leak:g_malloc
leak:g_slice_alloc
leak:libglib*.so*
leak:libgtk*.so*

# Threading libraries
leak:pthread_create
leak:libpthread.so*

# X11/GUI libraries
leak:libX11.so*
leak:libQt*.so*

Dynamic Suppression Management

# Use environment variable
export LSAN_OPTIONS="suppressions=/path/to/suppressions.txt"

# Multiple suppression files
export LSAN_OPTIONS="suppressions=global.supp:suppressions=local.supp"

# Print suppression statistics
export LSAN_OPTIONS="print_suppressions=1"

Suppression Best Practices

  1. Be Specific: Use precise patterns to avoid over-suppression
  2. Document Reasons: Add comments explaining why each suppression is needed
  3. Regular Review: Periodically review suppressions for relevance
  4. Version Control: Keep suppression files in version control
  5. Team Coordination: Share suppression files across development team

Suppression Validation

// test-suppressions.c
#include <stdlib.h>

// Function that should be suppressed
void suppressed_leak() {
    malloc(64); // This should be suppressed
}

// Function that should NOT be suppressed
void real_leak() {
    malloc(128); // This should be reported
}

int main() {
    suppressed_leak();
    real_leak();
    return 0;
}
# Test suppression effectiveness
echo "leak:suppressed_leak" > test.supp
LSAN_OPTIONS="suppressions=test.supp:print_suppressions=1" ./test-suppressions

Integration Strategies

Unit Test Integration

Google Test Integration

// test_with_lsan.cpp
#include <gtest/gtest.h>
#include <sanitizer/lsan_interface.h>

class LeakDetectionTest : public ::testing::Test {
protected:
    void SetUp() override {
        // Enable leak detection for this test
        __lsan_enable();
    }
    
    void TearDown() override {
        // Check for leaks after each test
        __lsan_do_recoverable_leak_check();
    }
};

TEST_F(LeakDetectionTest, NoLeaks) {
    char* ptr = new char[100];
    delete[] ptr;  // Properly cleaned up
}

TEST_F(LeakDetectionTest, DetectLeak) {
    // This test will fail due to leak
    EXPECT_DEATH({
        char* ptr = new char[100];
        // Intentional leak for testing
        __lsan_do_recoverable_leak_check();
    }, "LeakSanitizer: detected memory leaks");
}

CMake Integration

# CMakeLists.txt
if(ENABLE_LEAK_DETECTION)
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=leak -g")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=leak -g")
    
    # Create test target with leak detection
    add_executable(leak_tests ${TEST_SOURCES})
    target_compile_options(leak_tests PRIVATE -fsanitize=leak)
    target_link_options(leak_tests PRIVATE -fsanitize=leak)
    
    # Set environment for tests
    set_property(TEST leak_tests PROPERTY ENVIRONMENT 
        "LSAN_OPTIONS=suppressions=${CMAKE_SOURCE_DIR}/.lsan-suppressions")
endif()

CI/CD Pipeline Integration

GitHub Actions

name: Leak Detection

on: [push, pull_request]

jobs:
  leak-detection:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Install Clang
      run: |
        sudo apt-get update
        sudo apt-get install -y clang-15 llvm-15
    
    - name: Build with LeakSanitizer
      run: |
        export CC=clang-15
        export CXX=clang++-15
        cmake -DCMAKE_BUILD_TYPE=Debug \
              -DCMAKE_C_FLAGS="-fsanitize=leak -g" \
              -DCMAKE_CXX_FLAGS="-fsanitize=leak -g" \
              .
        make -j$(nproc)
    
    - name: Run Tests with Leak Detection
      env:
        LSAN_OPTIONS: "suppressions=.lsan-suppressions:print_suppressions=1:exitcode=1"
      run: |
        ctest --output-on-failure --parallel $(nproc)
    
    - name: Upload Leak Reports
      if: failure()
      uses: actions/upload-artifact@v3
      with:
        name: leak-reports
        path: |
          *.log
          leak-*.txt

Jenkins Pipeline

pipeline {
    agent any
    
    environment {
        CC = 'clang'
        CXX = 'clang++'
        LSAN_OPTIONS = 'suppressions=.lsan-suppressions:exitcode=1'
    }
    
    stages {
        stage('Build') {
            steps {
                sh '''
                    cmake -DCMAKE_C_FLAGS="-fsanitize=leak -g" \
                          -DCMAKE_CXX_FLAGS="-fsanitize=leak -g" \
                          .
                    make -j$(nproc)
                '''
            }
        }
        
        stage('Test') {
            steps {
                sh 'ctest --output-on-failure'
            }
            post {
                always {
                    archiveArtifacts artifacts: '**/*leak*.log', allowEmptyArchive: true
                }
            }
        }
    }
}

Periodic Leak Checking

Long-running Service Integration

// service_with_leak_detection.c
#include <sanitizer/lsan_interface.h>
#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t perform_leak_check = 0;

void signal_handler(int sig) {
    if (sig == SIGUSR1) {
        perform_leak_check = 1;
    }
}

int main() {
    signal(SIGUSR1, signal_handler);
    
    while (1) {
        // Normal service operations
        process_requests();
        
        // Periodic leak checking
        if (perform_leak_check) {
            printf("Performing leak check...\n");
            __lsan_do_recoverable_leak_check();
            perform_leak_check = 0;
        }
        
        sleep(1);
    }
    
    return 0;
}
# Trigger leak check from external monitor
kill -USR1 $(pidof service_with_leak_detection)

Report Analysis and Processing

Automated Report Processing

#!/usr/bin/env python3
# process_leak_reports.py

import re
import sys
from collections import defaultdict

def parse_leak_report(report_text):
    """Parse LSAN leak report and extract structured information."""
    
    leaks = []
    current_leak = None
    
    for line in report_text.split('\n'):
        # Direct/Indirect leak detection
        leak_match = re.match(r'(Direct|Indirect) leak of (\d+) byte\(s\) in (\d+) object\(s\) allocated from:', line)
        if leak_match:
            if current_leak:
                leaks.append(current_leak)
            
            current_leak = {
                'type': leak_match.group(1),
                'bytes': int(leak_match.group(2)),
                'objects': int(leak_match.group(3)),
                'stack_trace': []
            }
        
        # Stack trace lines
        stack_match = re.match(r'\s*#\d+\s+0x[0-9a-f]+\s+in\s+(.+)', line)
        if stack_match and current_leak:
            current_leak['stack_trace'].append(stack_match.group(1))
    
    if current_leak:
        leaks.append(current_leak)
    
    return leaks

def generate_summary(leaks):
    """Generate leak summary statistics."""
    
    summary = {
        'total_leaks': len(leaks),
        'total_bytes': sum(leak['bytes'] for leak in leaks),
        'direct_leaks': len([l for l in leaks if l['type'] == 'Direct']),
        'indirect_leaks': len([l for l in leaks if l['type'] == 'Indirect']),
        'by_function': defaultdict(int)
    }
    
    for leak in leaks:
        if leak['stack_trace']:
            func_name = leak['stack_trace'][0].split()[0]
            summary['by_function'][func_name] += leak['bytes']
    
    return summary

if __name__ == '__main__':
    report_file = sys.argv[1]
    
    with open(report_file, 'r') as f:
        report_text = f.read()
    
    leaks = parse_leak_report(report_text)
    summary = generate_summary(leaks)
    
    print(f"Leak Summary:")
    print(f"  Total leaks: {summary['total_leaks']}")
    print(f"  Total bytes: {summary['total_bytes']}")
    print(f"  Direct leaks: {summary['direct_leaks']}")
    print(f"  Indirect leaks: {summary['indirect_leaks']}")
    print(f"\nTop leak sources:")
    
    for func, bytes_leaked in sorted(summary['by_function'].items(), 
                                   key=lambda x: x[1], reverse=True)[:10]:
        print(f"  {func}: {bytes_leaked} bytes")

Comparison with Alternatives

LSAN vs AddressSanitizer (ASAN)

Aspect LSAN Standalone LSAN + ASAN
Purpose Leak detection only Memory errors + leaks
Runtime Overhead Minimal ~2x slowdown
Memory Overhead Low High (shadow memory)
Detection Scope Memory leaks Use-after-free, buffer overflows, leaks
Best Use Case Performance-sensitive leak testing Comprehensive memory debugging

When to use LSAN standalone:

  • Performance-critical applications that need leak detection
  • Already have other memory error detection tools
  • CI/CD pipelines with time constraints
  • Focus specifically on memory management validation

When to use LSAN with ASAN:

  • Comprehensive memory debugging during development
  • Early stage development with multiple memory issues
  • Integration testing where broader error detection is valuable

LSAN vs Valgrind Memcheck

Aspect LeakSanitizer Valgrind Memcheck
Compilation Requires recompilation Works with existing binaries
Runtime Overhead Minimal during execution 10-50x slowdown
Memory Overhead Low High
Platform Support Limited (Linux, macOS) Broad platform support
Accuracy High (definitive) Very High
False Positives Very Low Low
Integration Compile-time Runtime tool
# LSAN approach
clang -fsanitize=leak -g program.c
./program

# Valgrind approach  
gcc -g program.c
valgrind --leak-check=full ./program

LSAN Advantages:

  • Much faster execution
  • Lower memory overhead
  • Better integration with CI/CD
  • Part of standard toolchain

Valgrind Advantages:

  • No recompilation required
  • More detailed analysis
  • Broader platform support
  • Can analyze third-party binaries

LSAN vs Heap Profilers (tcmalloc, jemalloc)

Aspect LeakSanitizer Heap Profilers
Detection Type Definitive leaks Memory usage patterns
Production Use Development only Production capable
Overhead Minimal runtime Configurable overhead
Reporting Exit-time reports Continuous profiling
Integration Compile-time Runtime configuration
# LSAN - definitive leak detection
LSAN_OPTIONS="report_objects=1" ./program

# tcmalloc - heap profiling  
HEAPPROFILE=/tmp/heap ./program
pprof --web /tmp/heap.profile

LSAN vs tcmalloc HeapChecker:

  • LSAN: More accurate, better error messages, part of LLVM
  • HeapChecker: Runtime configurable, production-ready, Google-specific

LSAN vs Static Analysis Tools

Aspect LeakSanitizer Static Analysis
Analysis Time Runtime Compile-time
False Positives Very Low Can be high
Code Coverage Execution-dependent Complete code analysis
Dynamic Behavior Captures runtime leaks Misses dynamic patterns
Setup Complexity Simple compilation flag Tool configuration required

Complementary Usage:

# Static analysis first
clang-static-analyzer program.c

# Then runtime validation
clang -fsanitize=leak program.c && ./program

Use Cases

Development Testing

Early Development Phase

// prototype.c - Early development with leak detection
#include <stdlib.h>
#include <sanitizer/lsan_interface.h>

void experimental_feature() {
    // Disable leak detection for experimental code
    __lsan_disable();
    
    void *experimental_alloc = malloc(1024);
    // Experimental code that may leak
    
    __lsan_enable();
}

int main() {
    experimental_feature();
    
    // Core functionality with leak detection
    char *buffer = malloc(256);
    // ... use buffer
    free(buffer);
    
    return 0;
}

Unit Test Development

// memory_manager_test.cpp
#include <gtest/gtest.h>
#include <sanitizer/lsan_interface.h>
#include "memory_manager.h"

class MemoryManagerTest : public ::testing::Test {
protected:
    void TearDown() override {
        // Verify no leaks after each test
        __lsan_do_recoverable_leak_check();
    }
};

TEST_F(MemoryManagerTest, AllocateAndFree) {
    MemoryManager manager;
    void* ptr = manager.allocate(1024);
    ASSERT_NE(ptr, nullptr);
    manager.deallocate(ptr);
    // LSAN will verify no leaks
}

TEST_F(MemoryManagerTest, PoolGrowth) {
    MemoryManager manager;
    std::vector<void*> allocations;
    
    // Trigger pool growth
    for (int i = 0; i < 1000; ++i) {
        allocations.push_back(manager.allocate(64));
    }
    
    // Cleanup all allocations
    for (void* ptr : allocations) {
        manager.deallocate(ptr);
    }
    // LSAN will verify no leaks from pool management
}

CI/CD Validation

Pre-commit Hook

#!/bin/bash
# .git/hooks/pre-commit

echo "Running leak detection tests..."

# Build with leak detection
export CC=clang
export CFLAGS="-fsanitize=leak -g -O0"
make clean && make tests

# Run tests with leak detection
export LSAN_OPTIONS="suppressions=.lsan-suppressions:exitcode=1"
if ! make test; then
    echo "ERROR: Memory leaks detected!"
    echo "Please fix leaks before committing."
    exit 1
fi

echo "Leak detection passed ✓"

Pull Request Validation

# .github/workflows/pr-validation.yml
name: PR Memory Leak Check

on:
  pull_request:
    branches: [ main, develop ]

jobs:
  leak-check:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Build Environment
      run: |
        sudo apt-get update
        sudo apt-get install -y clang llvm
    
    - name: Build with Leak Detection
      run: |
        export CC=clang
        export CXX=clang++
        cmake -DCMAKE_BUILD_TYPE=Debug \
              -DCMAKE_C_FLAGS="-fsanitize=leak -g" \
              -DCMAKE_CXX_FLAGS="-fsanitize=leak -g" \
              -DENABLE_TESTING=ON \
              .
        make -j$(nproc)
    
    - name: Run Leak Detection Tests
      env:
        LSAN_OPTIONS: "suppressions=.lsan-suppressions:print_suppressions=1:exitcode=1"
      run: |
        # Run all tests with leak detection
        ctest --output-on-failure --parallel 2
        
        # Run specific leak-prone modules
        ./test/memory_management_test
        ./test/resource_lifecycle_test
        ./test/connection_pool_test
    
    - name: Analyze Results
      if: failure()
      run: |
        echo "Memory leaks detected in PR!"
        echo "Please review and fix before merging."

Pre-release Testing

Release Candidate Validation

#!/bin/bash
# release-validation.sh

set -e

VERSION=$1
if [ -z "$VERSION" ]; then
    echo "Usage: $0 <version>"
    exit 1
fi

echo "Validating release candidate $VERSION for memory leaks..."

# Build release candidate with leak detection
export CC=clang
export CXX=clang++
export CFLAGS="-fsanitize=leak -g -O2"
export CXXFLAGS="-fsanitize=leak -g -O2"

# Build
cmake -DCMAKE_BUILD_TYPE=Release \
      -DVERSION=$VERSION \
      .
make -j$(nproc)

# Configure leak detection
export LSAN_OPTIONS="suppressions=release.lsan-suppressions:max_leaks=0:exitcode=1"

echo "Running comprehensive test suite..."

# Core functionality tests
./test/unit_tests
./test/integration_tests
./test/performance_tests

# Long-running stability tests
timeout 300s ./test/stability_test || true

# Memory-intensive workloads
./test/stress_test --memory-intensive

echo "Running real-world scenario tests..."

# Simulate production workloads
./benchmark/realistic_workload --duration=60s
./benchmark/connection_lifecycle --iterations=1000

echo "Release candidate $VERSION passed leak detection validation ✓"

Staging Environment Testing

# staging-leak-test.sh
#!/bin/bash

# Deploy application with leak detection to staging
export LSAN_OPTIONS="suppressions=/app/config/staging.lsan-suppressions:exitcode=0"

# Start application
./app --config=staging.conf &
APP_PID=$!

# Run test scenarios
./test/api_smoke_test --host=staging.example.com
./test/load_test --duration=300s --host=staging.example.com

# Trigger leak check
kill -TERM $APP_PID
wait $APP_PID

# Check exit code
if [ $? -eq 23 ]; then
    echo "WARNING: Memory leaks detected in staging environment"
    # Send alert but don't fail deployment
fi

Regression Testing

Memory Regression Detection

#!/usr/bin/env python3
# memory_regression_test.py

import subprocess
import json
import sys
from pathlib import Path

def run_leak_test(binary_path, test_name):
    """Run leak detection test and parse results."""
    
    env = {
        'LSAN_OPTIONS': 'exitcode=0:print_suppressions=1:report_objects=1'
    }
    
    result = subprocess.run([binary_path], 
                          env=env, 
                          capture_output=True, 
                          text=True)
    
    # Parse leak information from output
    leak_bytes = 0
    leak_count = 0
    
    for line in result.stderr.split('\n'):
        if 'SUMMARY: AddressSanitizer:' in line:
            # Extract leak information
            parts = line.split()
            if len(parts) >= 2:
                leak_bytes = int(parts[1])
                
    return {
        'test_name': test_name,
        'leak_bytes': leak_bytes,
        'leak_count': leak_count,
        'exit_code': result.returncode
    }

def load_baseline(baseline_file):
    """Load baseline memory usage."""
    if Path(baseline_file).exists():
        with open(baseline_file, 'r') as f:
            return json.load(f)
    return {}

def save_baseline(baseline_file, results):
    """Save current results as new baseline."""
    with open(baseline_file, 'w') as f:
        json.dump(results, f, indent=2)

def main():
    tests = [
        ('basic_functionality', './test/basic_test'),
        ('connection_pool', './test/connection_test'),
        ('cache_manager', './test/cache_test'),
        ('request_handler', './test/handler_test')
    ]
    
    baseline_file = 'memory_baseline.json'
    baseline = load_baseline(baseline_file)
    current_results = {}
    
    regression_detected = False
    
    for test_name, binary in tests:
        print(f"Running {test_name}...")
        result = run_leak_test(binary, test_name)
        current_results[test_name] = result
        
        # Check for regression
        if test_name in baseline:
            baseline_bytes = baseline[test_name]['leak_bytes']
            current_bytes = result['leak_bytes']
            
            if current_bytes > baseline_bytes:
                print(f"REGRESSION: {test_name} leaked {current_bytes} bytes "
                      f"(baseline: {baseline_bytes} bytes)")
                regression_detected = True
            elif current_bytes < baseline_bytes:
                print(f"IMPROVEMENT: {test_name} leaked {current_bytes} bytes "
                      f"(baseline: {baseline_bytes} bytes)")
        
        print(f"  Result: {result['leak_bytes']} bytes leaked")
    
    # Update baseline if no regressions
    if not regression_detected:
        save_baseline(baseline_file, current_results)
        print("Baseline updated with current results")
    
    sys.exit(1 if regression_detected else 0)

if __name__ == '__main__':
    main()

Automated Regression Monitoring

# .github/workflows/memory-regression.yml
name: Memory Regression Monitor

on:
  schedule:
    - cron: '0 2 * * *'  # Daily at 2 AM
  
  workflow_dispatch:

jobs:
  memory-regression:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0  # Need history for comparison
    
    - name: Setup Environment
      run: |
        sudo apt-get update
        sudo apt-get install -y clang llvm python3
    
    - name: Build Current Version
      run: |
        export CC=clang
        cmake -DCMAKE_C_FLAGS="-fsanitize=leak -g" .
        make -j$(nproc)
    
    - name: Restore Baseline
      uses: actions/cache@v3
      with:
        path: memory_baseline.json
        key: memory-baseline-${{ runner.os }}
    
    - name: Run Regression Tests
      run: |
        python3 scripts/memory_regression_test.py
    
    - name: Report Results
      if: failure()
      run: |
        echo "Memory regression detected!"
        echo "Please investigate recent changes."
    
    - name: Save Updated Baseline
      if: success()
      uses: actions/cache@v3
      with:
        path: memory_baseline.json
        key: memory-baseline-${{ runner.os }}

Limitations

Compilation Requirements

Recompilation Mandatory:

# Cannot use with existing binaries
gcc program.c -o program        # Won't work with LSAN
./program                       

# Must recompile with LSAN flags
clang -fsanitize=leak program.c -o program  # Required
./program

Build System Integration Challenges:

  • All dependencies must be built with compatible flags
  • Third-party libraries may need rebuilding
  • Complex build systems require extensive modifications
  • Cross-compilation complexity increases

Toolchain Dependencies:

# Requires LLVM/Clang toolchain
sudo apt-get install clang llvm  # Linux
brew install llvm               # macOS

# GCC support limited
gcc -fsanitize=leak program.c   # May not work on all systems

Stop-the-World Behavior

Process Suspension During Detection:

// program.c - Demonstrates stop-the-world impact
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

void* worker_thread(void* arg) {
    while (1) {
        // Critical work that cannot be interrupted
        perform_critical_operation();
        usleep(100000);  // 100ms
    }
    return NULL;
}

int main() {
    pthread_t thread;
    pthread_create(&thread, NULL, worker_thread, NULL);
    
    // Allocate memory that will be leaked
    malloc(100);
    
    // At program exit, ALL threads suspend during leak detection
    // Critical operations may be interrupted
    return 0;  // Stop-the-world leak detection here
}

Real-time System Impact:

  • Unpredictable pause times during leak detection
  • Can violate real-time constraints
  • Network connections may timeout
  • Critical operations may be disrupted

Mitigation Strategies:

// Controlled leak checking
#include <sanitizer/lsan_interface.h>

void controlled_shutdown() {
    // Prepare for leak detection
    stop_background_threads();
    flush_all_buffers();
    close_network_connections();
    
    // Now safe to perform leak detection
    __lsan_do_recoverable_leak_check();
}

Production Environment Restrictions

Security Considerations:

  • LSAN runtime not security-hardened
  • Debug symbols expose implementation details
  • Memory layout information leakage
  • Potential attack surface expansion
# Production build (what NOT to do)
clang -fsanitize=leak -O2 server.c -o production_server  # NEVER!

# Proper production build
clang -O2 -DNDEBUG server.c -o production_server         # Correct

Performance Impact:

  • Metadata overhead for allocation tracking
  • Memory consumption increase
  • Longer process startup/shutdown times
  • Potential memory fragmentation

Operational Concerns:

# Production monitoring (problematic)
LSAN_OPTIONS="exitcode=1" ./production_service
# Service dies on leak detection!

# Better approach: separate testing environment
docker run test_image:lsan-enabled ./service

Platform Support Limitations

Limited Platform Coverage:

# Supported platforms (as of 2024)
- Linux (x86_64, aarch64) ✓
- macOS (x86_64, Apple Silicon) ✓  
- Android ✓
- Fuchsia ✓
- NetBSD ✓

# Unsupported/Limited
- Windows ❌
- FreeBSD (limited)
- Other Unix variants (varies)

Architecture Restrictions:

# Check platform support
#if defined(__has_feature)
#  if __has_feature(address_sanitizer)
#    define LSAN_AVAILABLE 1
#  endif
#endif

#ifdef LSAN_AVAILABLE
#include <sanitizer/lsan_interface.h>
#else
// Fallback for unsupported platforms
void __lsan_do_recoverable_leak_check() { /* no-op */ }
#endif

macOS Specific Limitations:

# macOS requires explicit enabling
export ASAN_OPTIONS=detect_leaks=1  # Required on macOS

# Some system frameworks may cause issues
export LSAN_OPTIONS="suppressions=macos.supp"

False Negative Scenarios

Conservative Pointer Analysis:

// false_negative.c - Cases LSAN might miss
#include <stdlib.h>
#include <stdint.h>

int main() {
    char *ptr = malloc(100);
    
    // Store pointer in non-obvious way
    uintptr_t disguised = (uintptr_t)ptr ^ 0xDEADBEEF;
    
    // LSAN may not recognize this as a pointer
    ptr = NULL;  // Original pointer lost
    
    // Leak not detected due to disguised reference
    return 0;
}

Thread-Local Storage Issues:

// tls_leak.c - Thread-local storage complications
#include <pthread.h>
#include <stdlib.h>

__thread void *tls_ptr = NULL;

void* thread_func(void* arg) {
    tls_ptr = malloc(200);
    // Thread exits without cleanup
    return NULL;
}

int main() {
    pthread_t thread;
    pthread_create(&thread, NULL, thread_func, NULL);
    pthread_join(thread, NULL);
    
    // TLS leak might not be detected
    return 0;
}

Timing-Dependent Leaks:

  • Race conditions in leak detection
  • Memory freed after leak check starts
  • Asynchronous cleanup operations
  • Signal handler complications

Memory Overhead Considerations

Allocation Metadata:

// Each allocation has tracking overhead
void *ptr = malloc(8);  // 8 bytes requested
// LSAN adds metadata for tracking
// Actual memory usage > 8 bytes

Shadow Memory Usage:

  • Additional memory for tracking allocations
  • Metadata scales with number of allocations
  • Can increase total memory usage by 20-50%

Mitigation:

# Monitor memory usage during testing
LSAN_OPTIONS="report_objects=1" valgrind --tool=massif ./program

# Use memory limits in testing
ulimit -v 1048576  # Limit virtual memory to 1GB
./program_with_lsan

These limitations highlight why LSAN is specifically designed for development and testing environments rather than production deployment. Understanding these constraints is crucial for effective integration into development workflows.


Document Information:

⚠️ **GitHub.com Fallback** ⚠️