Memory Technologies Development Only LSan - antimetal/system-agent GitHub Wiki
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
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
LSAN employs a sophisticated approach to detect memory leaks through reachability analysis:
- Intercepts all memory allocation calls (malloc, new, etc.)
- Maintains metadata about each allocation
- Tracks allocation size, location, and call stack
- 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)
- Direct leaks: Unreachable allocations
- Indirect leaks: Allocations only reachable through direct leaks
- Possible leaks: Memory that may be leaked (ambiguous pointers)
- Program runs normally with minimal instrumentation
- At exit, LSAN suspends all threads
- Scans memory regions for pointers to allocated blocks
- Builds reachability graph
- Reports unreachable allocations as leaks
- Provides stack traces for leak origins
LSAN is designed for development and testing environments, not production systems:
# 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
# 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
- Integration with existing test suites
- Automated leak detection in staging environments
- Performance regression testing
- Memory usage baseline establishment
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
# 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
# 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
- Must use
clang
/clang++
for linking (notld
directly) - Sanitizer runtime library automatically linked
- Ensure proper symbolication with
-g
flag - Frame pointers recommended:
-fno-omit-frame-pointer
# Dynamic linking (default)
clang -fsanitize=leak program.c
# Static linking (if needed)
clang -fsanitize=leak -static-libsan program.c
- Sanitizer runtime must be available for target platform
- May require building LLVM compiler-rt for target
- Limited platform support compared to native builds
- 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
// 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).
- File-based suppression rules
- Pattern matching against stack traces
- Function, file, and library name matching
- Hierarchical suppression rules
#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);
// 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
}
# 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
# .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
#!/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-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;
}
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"
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) |
# 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"
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"
# Custom symbolizer path
export LSAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer
# Symbolization options
LSAN_OPTIONS="symbolize=1:print_module_map=1"
# Linux-specific
LSAN_OPTIONS="use_tls=1:use_ld_allocations=1"
# macOS-specific (when supported)
ASAN_OPTIONS="detect_leaks=1" # Enable on macOS
=================================================================
==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).
- 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
- 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
- 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
#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
-----------------------------------------------------
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 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 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*
- 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
# 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
# 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*
# 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"
- Be Specific: Use precise patterns to avoid over-suppression
- Document Reasons: Add comments explaining why each suppression is needed
- Regular Review: Periodically review suppressions for relevance
- Version Control: Keep suppression files in version control
- Team Coordination: Share suppression files across development team
// 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
// 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");
}
# 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()
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
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
}
}
}
}
}
// 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)
#!/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")
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
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
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
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
// 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;
}
// 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
}
#!/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 ✓"
# .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."
#!/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-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
#!/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()
# .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 }}
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
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();
}
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
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"
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
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:
-
File:
/Users/john/am/notes/memory-analysis/technologies/lsan.md
- Category: Memory Analysis Technologies
- Last Updated: 2024
- Related: AddressSanitizer, Valgrind, Memory Leak Detection Comparison