Clang and GCC 4.9implemented LeakSanitizer in 2013. LeakSanitizer(LSan) is a memory leak detector. It intercepts memory allocationfunctions and by default detects memory leaks at atexit
time. The implementation is purely in the runtime(compiler-rt/lib/lsan
) and no instrumentation isneeded.
LSan has very little architecture-specific code and supports many64-bit targets. Some 32-bit targets (e.g. Linux arm/x86-32) aresupported as well, but there may be high false negatives because of thesmall pointer size. Every supported operating system needs to providesome way to "stop the world".
LSan can be used in 3 ways.
-fsanitize=leak
)-fsanitize=address
)-fsanitize=hwaddress
)The most common way to use LSan isclang -fsanitize=address
(or GCC). For LSan-supportedtargets (#define CAN_SANITIZE_LEAKS 1
), theAddressSanitizer (ASan) runtime enables LSan by default.
1 | % cat a.c |
As a runtime-only feature, -fsanitize=leak
is mainly forlink actions. When linking an executable, with the default-static-libsan
mode on many targets, Clang Driver passes--whole-archive $resource_dir/lib/$triple/libclang_rt.lsan.a --no-whole-archive
to the linker. GCC and some platforms prefer shared runtime/dynamicruntime. See Allabout sanitizer interceptors. 1
2
3
4% clang -fsanitize=leak a.o '-###' |& grep --color lsan
... --whole-archive" "/tmp/Rel/lib/clang/17/lib/x86_64-unknown-linux-gnu/libclang_rt.lsan.a" "--no-whole-archive" ...
% gcc -fsanitize=leak a.o '-###' |& grep --color lsan
... /usr/lib/gcc/x86_64-linux-gnu/12/liblsan_preinit.o --push-state --no-as-needed -llsan ...
-fsanitize=leak
affects compile actions as well, butonly for the following C/C++ preprocessor feature. 1
2
3
...
Standalone LSan intercepts malloc-family functions. It uses thetemporized SizeClassAllocator{32,64}
with chunk metadata.The interceptors record the allocation information (requested size,stack trace).
AddressSanitizer already intercepts malloc-family functions. Itschunk metadata has extra bits for LSan.
By default, the common options detect_leaks
andleak_check_at_exit
are enabled. The runtime installs a hookwith atexit
which will perform the leak check.Alternatively, the user can call __lsan_do_leak_check
torequest a leak check before the exit time.
Upon a leak check, the runtime performs a job very similar to amark-and-sweep garbage collection algorithm. It suspends all threads("stop the world") and scans root regions (find allocations reachable bythe process). Root regions include:
__lsan_ignore_object
or__lsan_disable
__lsan::ProcessGlobalRegions
). OnLinux, these refer to memory mappings due to writablePT_LOAD
program headers. This ensures that allocationsreachable by a global variable are not leaks.use_registers=1
),stack (default use_stack=1
), thread-local storage (defaultuse_tls=1
), and additional pointers in the threadcontext__lsan_register_root_region
The runtimes uses a flood fill algorithm to find reachableallocations from a region. The runtime is conservative and scans allaligned words which look like a pointer. (It is not feasible todetermine whether a pointer-like word is used as an integer/floatingpoint number, not as a pointer.) If the word looks like a pointer into aheap (many 64-bit targets use[0x600000000000, 0x640000000000)
), the runtime checkswhether it refers to an active chunk. In Valgrind's terms, a referencecan be a "start-pointer" (a pointer to the start of the chunk) or an"interior-pointer" (a pointer to the middle of the chunk).
Finally, the runtime iterates over all active allocations and reportsleaks for unmarked allocations.
Each allocation reserves 2 bits to record its state:leaked/reachable/ignored. For better diagnostics, "leaked" can be director indirect. If a chunk is marked as leaked, all chunks reachable fromit are marked as indirectly leaked. (*p = malloc(43);
inthe very beginning example is an indirect leak.)
1 | enum ChunkTag { |
Standalone LSan uses this chunk metadata struct: 1
2
3
4
5
6
7
8
9
10
11struct ChunkMetadata {
u8 allocated : 8; // Must be first.
ChunkTag tag : 2;
uptr requested_size : 54;
uptr requested_size : 32;
uptr padding : 22;
u32 stack_trace_id;
};
ASan just stores a 2-bit ChunkTag
in its existing chunkmetadata (__asan::Chunkheader::lsan_tag
). Similarly, HWASanstores a 2-bit ChunkTag
in its existing chunk metadata(__hwasan::Metadata::lsan_tag
).
Both standalone LSan and ASan use the temporizedSizeClassAllocator{32,64}
(primary allocator) with athread-local cache (SizeClassAllocator{32,64}LocalCache
).The cache defines many size classes and maintains free lists for theseclasses. Upon an allocation request, if the free list for the requestedsize class is empty, the cache will call Refill
to grabmore chunks from SizeClassAllocator{32,64}
. If the freelist is not empty, the cache hands over a chunk from the free list tothe user.
SizeClassAllocator64
has a total allocator space ofkSpaceSize
bytes. The space is split into multiple regionsof the same size (kSpaceSize
), each serving a single sizeclass. When the cache calls Refill
,SizeClassAllocator64
takes a lock of the region and callsmmap
to allocate a new memory mapping (and ifkMetadataSize!=0
, another memory mapping for metadata). Foran active allocation, it is very efficient to compute its index in theregion and its associated metadata.
On Linux, the runtime invokes the clone syscall to create a tracerthread. The tracer thread calls ptrace
withPTRACE_ATTACH
to stop every other thread.
On Fuchsia, the runtime calls__sanitizer_memory_snapshot
to stop the world.
Specify the environment variable LSAN_OPTIONS
to toggleruntime behaviors.
For standalone LSan, exitcode=23
is the default. Theruntime calls _exit(23)
upon leaks. For ASan-integratedLSan, exitcode=1
is the default.
LSAN_OPTIONS=use_registers=0:use_stack=0:use_tls=0
canremove some default root regions.
leak_check_at_exit=0
disables registering anatexit
hook for leak checking.
report_objects=1
reports the addresses of individualleaked objects.
detect_leaks=0
disables all leak checking, includinguser-requested ones due to __lsan_do_leak_check
or__lsan_do_recoverable_leak_check
. This is similar todefiningextern "C" int __lsan_is_turned_off() { return 1; }
in theprogram. Using standalone LSan with detect_leaks=0
has aperformance characteristic similar to using pureSizeClassAllocator{32,64}
and has nearly no extra overheadexcept the stack trace. If we can get rid of the stack unwindingoverhead, we will have a simple one benchmarkingSizeClassAllocator{32,64}
's performance.
If __lsan_default_options
is defined, the return valuewill be parsed like LSAN_OPTIONS
.
Call __lsan_ignore_object
to ignore an allocation.__lsan_disable
ignores all allocations for the currentthread until __lsan_enable
is called.
The runtime scans each ignored allocation. An allocation reachable byan ignored allocation is not considered as a leak.
__lsan_register_root_region
registers a region as aroot. The runtime scans the intersection of the region and valid memorymappings (/proc/self/maps
on Linux).LSAN_OPTIONS=use_root_regions=0
can disable the registeredregions.
We can provide a suppression file withLSAN_OPTIONS=suppressions=a.supp
. The file contains onesuppression rule per line, each rule being of the formleak:<pattern>
. For a leak, the runtime checks everyframe in the stack trace. A frame has a module name associated with thecall site address (executable or shared object), and when symbolizationis available, a source file name and a function name. If any modulename/source file name/function name matches a pattern (substringmatching using glob), the leak is suppressed.
Note: symbolization requires debug information and a symbolizer(internal symbolizer (not built by default) or anllvm-symbolizer
in a PATH
directory).
Let's see an example. 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22cat > a.c <<'eof'
#include <stdlib.h>
#include <stdio.h>
void *foo();
int main() {
foo();
printf("%p\n", malloc(42));
}
eof
cat > b.c <<'eof'
#include <stdlib.h>
void *foo() { return malloc(42); }
eof
clang -fsanitize=leak -fpic -g -shared b.c -o b.so
clang -fsanitize=leak -g a.c ./b.so -o a
which llvm-symbolizer # available
LSAN_OPTIONS=suppressions=<(printf 'leak:a') ./a # suppresses both leaks (by module name)
LSAN_OPTIONS=suppressions=<(printf 'leak:a.c') ./a # suppresses both leaks (by source file name)
LSAN_OPTIONS=suppressions=<(printf 'leak:b.c') ./a # suppresses the leak in b.so (by source file name)
LSAN_OPTIONS=suppressions=<(printf 'leak:main') ./a # suppresses both leaks (by function name)
LSAN_OPTIONS=suppressions=<(printf 'leak:foo') ./a # suppresses the leak in b.so (by function name)
Standalone LeakSanitizer can be used with SanitizerCoverage:clang -fsanitize=leak -fsanitize-coverage=func,trace-pc-guard a.c
The testsuite has been moved. Usegit log -- compiler-rt/lib/lsan/lit_tests/TestCases/pointer_to_self.cc
to do archaeology for old tests.
Valgrind's Memcheck defaults to --leak-check=yes
andperforms leak checking. The feature has been available since the initialrevision (git log -- vg_memory.c
) in 2002.
Google open sourced HeapLeakChecker as part of gperftools. It is partof TCMalloc and used with debugallocation.cc
.Multi-threading allocations need to grab a global lock and are muchslower than a sanitizer allocator.heap_check_max_pointer_offset
(default: 2048) specifies thelargest offset it scans an allocation for pointers. The default makes itvulnerable to false positives.
heaptrack is a heapmemory profiler which supports leak checking.