Back to TILs

How to detect C/C++ memory leak

Date: 2022-12-30Last modified: 2024-02-26

Table of contents

AddressSanitizer (or ASan) is a tool developed by Google detect memory access error such as use-after-free and memory leaks. It can be used on both C and C++ codes. Address Sanitizer uses runtime instrumentation to track memory allocations, which mean you must build your code with Address Sanitizer to take advantage of it’s features.

Compiler version

AddressSanitizer is currently implemented in:

Instrumentation cost

On average, the instrumentation increases processing time by 73% and memory usage by 240% according to AddressSanitizer: A Fast Address Sanity Checker.

Limitations

AddressSanitizer does not detect any uninitialized memory reads (but this is detected by MemorySanitizer), and only detects some use-after-return bugs.

It is also not capable of detecting all arbitrary memory corruption bugs, nor all arbitrary write bugs due to integer underflow/overflows (when the integer with undefined behavior is used to calculate memory address offsets).

Adjacent buffers in structs and classes are not protected from overflow, in part to prevent breaking backwards compatibility.

Makefile

The code instrumentation is made by adding -fsanitize=address -g -O to the compilation flags.

The lines below are used to build and run the code used in this article. The compiled files files are bin/leak_01 and bin/leak_01.2

# Build both versions (instrumented and non instrumented)
bin/leak_01: leak_01.cpp
	$(CXX) -Wall -fsanitize=address -O -g $^ -o $@
	$(CXX) -Wall $^ -o $@.2 # non instrumented build

# Save each function output to their respective files
output/leak_01.txt: bin/leak_01
	 bin/leak_01 0 > $@.0.i 2>&1 || echo "Error ouput expected"
	 bin/leak_01 1 > $@.1.i 2>&1 || echo "Error ouput expected"
	 bin/leak_01.2 0 > $@.0.ni
	 bin/leak_01.2 1 > $@.1.ni

Buggy C++ source code

C++ Delete Mismatch

Bad function which does not deallocate 4bytes (sizeof int) on each call.

#line 100 // for easy reference the line above in this post
void leak()
{
  int *ptr = new int{ 42 };
  cout << "The value of ptr is " << *ptr << " " << ptr << endl;
  // missing delete call here!
}

The main function will call the leak function 10 times leaking 40 bytes (10 × sizeof int).

The debug information printed on standard error output explain the fault.

Direct leak of 40 byte(s) in 10 object(s) allocated from:

Instrumented possible output

The value of ptr is 42 0x502000000010
The value of ptr is 42 0x502000000030
The value of ptr is 42 0x502000000050
The value of ptr is 42 0x502000000070
The value of ptr is 42 0x502000000090
The value of ptr is 42 0x5020000000b0
The value of ptr is 42 0x5020000000d0
The value of ptr is 42 0x5020000000f0
The value of ptr is 42 0x502000000110
The value of ptr is 42 0x502000000130

=================================================================
==3135698==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 40 byte(s) in 10 object(s) allocated from:
    #0 0x55f75cae574d in operator new(unsigned long) (/home/geraldo/git/Intmain/language_cpp/bin/leak_01+0x10274d) (BuildId: 8a1eeb90529a513e992fd2420cd060dc6ebe82a5)
    #1 0x55f75cae7a4d in leak() /home/geraldo/git/Intmain/language_cpp/leak_01.cpp:102:14
    #2 0x7fc2612461c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: 40 byte(s) leaked in 10 allocation(s).

Non instrumented possible output

The value of ptr is 42 0x5557c78e4eb0
The value of ptr is 42 0x5557c78e5ee0
The value of ptr is 42 0x5557c78e5f00
The value of ptr is 42 0x5557c78e5f20
The value of ptr is 42 0x5557c78e5f40
The value of ptr is 42 0x5557c78e5f60
The value of ptr is 42 0x5557c78e5f80
The value of ptr is 42 0x5557c78e5fa0
The value of ptr is 42 0x5557c78e5fc0
The value of ptr is 42 0x5557c78e5fe0

Heap use after free

#line 200
void heapUseAfterFree()
{
  int *array = new int[100];
  delete[] array;
  cout << "Heap use after free: " << array[42] << endl; // 💩
}

Instrumented possible output

=================================================================
==3135702==ERROR: AddressSanitizer: heap-use-after-free on address 0x5140000000e8 at pc 0x5589cad83dc2 bp 0x7ffe2534d210 sp 0x7ffe2534d208
READ of size 4 at 0x5140000000e8 thread T0
    #0 0x5589cad83dc1 in heapUseAfterFree() /home/geraldo/git/Intmain/language_cpp/leak_01.cpp:204:38
    #1 0x5589cad841e0 in main /home/geraldo/git/Intmain/language_cpp/leak_01.cpp:233:5
    #2 0x7fde246461c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #3 0x7fde24646284 in __libc_start_main csu/../csu/libc-start.c:360:3
    #4 0x5589cacab450 in _start (/home/geraldo/git/Intmain/language_cpp/bin/leak_01+0x2c450) (BuildId: 8a1eeb90529a513e992fd2420cd060dc6ebe82a5)

0x5140000000e8 is located 168 bytes inside of 400-byte region [0x514000000040,0x5140000001d0)
freed by thread T0 here:
    #0 0x5589cad820ad in operator delete[](void*) (/home/geraldo/git/Intmain/language_cpp/bin/leak_01+0x1030ad) (BuildId: 8a1eeb90529a513e992fd2420cd060dc6ebe82a5)
    #1 0x5589cad83c68 in heapUseAfterFree() /home/geraldo/git/Intmain/language_cpp/leak_01.cpp:203:3
    #2 0x7fde246461c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

previously allocated by thread T0 here:
    #0 0x5589cad8185d in operator new[](unsigned long) (/home/geraldo/git/Intmain/language_cpp/bin/leak_01+0x10285d) (BuildId: 8a1eeb90529a513e992fd2420cd060dc6ebe82a5)
    #1 0x5589cad83c5d in heapUseAfterFree() /home/geraldo/git/Intmain/language_cpp/leak_01.cpp:202:16
    #2 0x7fde246461c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

SUMMARY: AddressSanitizer: heap-use-after-free /home/geraldo/git/Intmain/language_cpp/leak_01.cpp:204:38 in heapUseAfterFree()
Shadow bytes around the buggy address:
  0x513ffffffe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x513ffffffe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x513fffffff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x513fffffff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x514000000000: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
=>0x514000000080: fd fd fd fd fd fd fd fd fd fd fd fd fd[fd]fd fd
  0x514000000100: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x514000000180: fd fd fd fd fd fd fd fd fd fd fa fa fa fa fa fa
  0x514000000200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x514000000280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x514000000300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==3135702==ABORTING

Non instrumented possible output

Heap use after free: 0

Main function

Each bad function can be called individually by passing its number.

int main( int argc, char **argv )
{
  if( argc != 2 ) {
    return 1;
  }
  switch( stoi( argv[1] ) ) {
  case 0:
    for( int i = 0; i < 10; i++ ) {
      leak();
    }
    break;
  case 1:
    heapUseAfterFree();
    break;
  }
  return 0;
}

References