Back to TILs

How to detect C/C++ memory leak

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 0x602000000010
The value of ptr is 42 0x602000000030
The value of ptr is 42 0x602000000050
The value of ptr is 42 0x602000000070
The value of ptr is 42 0x602000000090
The value of ptr is 42 0x6020000000b0
The value of ptr is 42 0x6020000000d0
The value of ptr is 42 0x6020000000f0
The value of ptr is 42 0x602000000110
The value of ptr is 42 0x602000000130

=================================================================
==1303120==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 40 byte(s) in 10 object(s) allocated from:
    #0 0x55867a40ddfd in operator new(unsigned long) (/data/home/geraldo/git/Intmain/language_cpp/bin/leak_01+0xf2dfd) (BuildId: 1d95569a78d0fb60f2eb761b1d185820fd6521d6)
    #1 0x55867a4103cd in leak() /home/geraldo/git/Intmain/language_cpp/leak_01.cpp:101:14
    #2 0x7f6126c9dd09 in __libc_start_main csu/../csu/libc-start.c:308:16

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

Non instrumented possible output

The value of ptr is 42 0x5565e3c94eb0
The value of ptr is 42 0x5565e3c95ee0
The value of ptr is 42 0x5565e3c95f00
The value of ptr is 42 0x5565e3c95f20
The value of ptr is 42 0x5565e3c95f40
The value of ptr is 42 0x5565e3c95f60
The value of ptr is 42 0x5565e3c95f80
The value of ptr is 42 0x5565e3c95fa0
The value of ptr is 42 0x5565e3c95fc0
The value of ptr is 42 0x5565e3c95fe0

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

=================================================================
==1303124==ERROR: AddressSanitizer: heap-use-after-free on address 0x6140000000e8 at pc 0x5573cf81a73b bp 0x7ffd07745c70 sp 0x7ffd07745c68
READ of size 4 at 0x6140000000e8 thread T0
    #0 0x5573cf81a73a in heapUseAfterFree() /home/geraldo/git/Intmain/language_cpp/leak_01.cpp:203:38
    #1 0x5573cf81ab60 in main /home/geraldo/git/Intmain/language_cpp/leak_01.cpp:231:7
    #2 0x7f84e8ce4d09 in __libc_start_main csu/../csu/libc-start.c:308:16
    #3 0x5573cf7443f9 in _start (/data/home/geraldo/git/Intmain/language_cpp/bin/leak_01+0x1f3f9) (BuildId: 1d95569a78d0fb60f2eb761b1d185820fd6521d6)

0x6140000000e8 is located 168 bytes inside of 400-byte region [0x614000000040,0x6140000001d0)
freed by thread T0 here:
    #0 0x5573cf81875d in operator delete[](void*) (/data/home/geraldo/git/Intmain/language_cpp/bin/leak_01+0xf375d) (BuildId: 1d95569a78d0fb60f2eb761b1d185820fd6521d6)
    #1 0x5573cf81a5e8 in heapUseAfterFree() /home/geraldo/git/Intmain/language_cpp/leak_01.cpp:202:3
    #2 0x7f84e8ce4d09 in __libc_start_main csu/../csu/libc-start.c:308:16

previously allocated by thread T0 here:
    #0 0x5573cf817f0d in operator new[](unsigned long) (/data/home/geraldo/git/Intmain/language_cpp/bin/leak_01+0xf2f0d) (BuildId: 1d95569a78d0fb60f2eb761b1d185820fd6521d6)
    #1 0x5573cf81a5dd in heapUseAfterFree() /home/geraldo/git/Intmain/language_cpp/leak_01.cpp:201:16
    #2 0x7f84e8ce4d09 in __libc_start_main csu/../csu/libc-start.c:308:16

SUMMARY: AddressSanitizer: heap-use-after-free /home/geraldo/git/Intmain/language_cpp/leak_01.cpp:203:38 in heapUseAfterFree()
Shadow bytes around the buggy address:
  0x613ffffffe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x613ffffffe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x613fffffff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x613fffffff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x614000000000: fa fa fa fa fa fa fa fa fd fd fd fd fd fd fd fd
=>0x614000000080: fd fd fd fd fd fd fd fd fd fd fd fd fd[fd]fd fd
  0x614000000100: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x614000000180: fd fd fd fd fd fd fd fd fd fd fa fa fa fa fa fa
  0x614000000200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x614000000280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x614000000300: 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
==1303124==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