>

Interpreting Sanitizer Messages

Credit due to Professor David Smallberg

When you build your program with Address Sanitizer, the compiler inserts extra code to check for certain types of undefined behavior. That code can't catch everything, but does a good job of catching most problems. When it detects an issue, it terminates your program and writes a lot of diagnostic output. Let's see how to read it.

Suppose we have the following program:

	double myFunction(double a[], int k)
	{
	    return a[k];
	}
	
	int main()
	{
	    double x[10];
	    double y[20];
	    double z[30];
	    x[5] = myFunction(y, 22);
	    z[5] = x[5];
	    if (x[5] != z[5])
	        return 1;
	}

If we run this, we get a wall of text starting with the following perhaps. (If you run it, some of the numbers might be different, but the relationships between them will be the same.)

=================================================================
==14177==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffcfbd6c3f0 at ...
READ of size 8 at 0x7ffcfbd6c3f0 thread T0
    #0 0x4009bb in myFunction(double*, int) (/tmp/a.out+0x4009bb)
    #1 0x400a98 in main (/tmp/a.out+0x400a98)
    #2 0x7ff6f49ec554 in __libc_start_main (/usr/lib64/libc.so.6+0x22554)
    #3 0x400878  (/tmp/a.out+0x400878)

Address 0x7ffcfbd6c3f0 is located in stack of thread T0 at offset 320 in frame
    #0 0x4009df in main (/tmp/a.out+0x4009df)

  This frame has 3 object(s):
    [32, 112) 'x' (line 8)
    [144, 304) 'y' (line 9) <== Memory access at offset 320 overflows this variable
    [368, 608) 'z' (line 10)

Let's see what this is telling us. stack-buffer-overflow usually means we tried to access an element past the end of a local array. READ of size 8 means we tried to get the value of some 8-byte object. (WRITE of size 8 would mean we tried to store a value into an 8-byte object.) What follows is a stack trace showing what function we were in when the bad event occurred, what function called it, what function called the function that called it, etc. From

    #0 ... in myFunction(double*, int) ...
    #1 ... in main ...
    ...

we see that we were in myFunction when the bad event occurred, and myFunction was directly called from main.

Now, we have

    ... on address 0x7ffcfbd6c3f0 ...
    ...
    Address 0x7ffcfbd6c3f0 is located ... at offset 320 in frame
    ... in main ...

telling us that the address that we tried to access is among the local variables of main, at "offset 320", and the objects in main, with their offsets, are listed here:

  This frame has 3 object(s):
    [32, 112) 'x' (line 8)
    [144, 304) 'y' (line 9)
    [368, 608) 'z' (line 10)

This tells us that the array x occupies offsets 32 up to but not including 112, a range of 112−32 = 80 bytes. Since x has 10 elements and doubles are 8 bytes long, this makes sense. Similarly y occupies offsets 144 up to but not including 304, which is 160 bytes, which is 20 doubles.

We see now that

    READ of size 8 at 0x7ffcfbd6c3f0 ...
    ...
    Address 0x7ffcfbd6c3f0 is located ... at offset 320 in frame
    ...
    [144, 304) 'y' (line 9) <== Memory access at offset 320 overflows this variable

brings it all together. Offset 320 is 320−144 = 176 bytes from the start of y, and 176 bytes divided by 8 bytes per double is 22 doubles: We were trying to get a value from y[22], but y has only 20 elements.

Here's another example. The program is

    #include <iostream>
    #include <cstring>
    using namespace std;
    
    void anotherFunction(const char* s)
    {
        for (int k = strlen(s); k >= 0; k--)
            cout << s[k-1] << endl;
    }
    
    int main()
    {
        anotherFunction("Hi");
    }

and it produces something starting with

i
H
=================================================================
==46331==ERROR: AddressSanitizer: global-buffer-overflow on address 0x00000040121f ...
READ of size 1 at 0x00000040121f ...
    #0 ... in anotherFunction(char const*) ...
    #1 ... in main ...
    ...

0x00000040121f is located 45 bytes to the right of global variable '*.LC0' defined in 'myTestProgram.cpp' (0x4011e0) of size 18
  '*.LC0' is ascii string 'myTestProgram.cpp'
0x00000040121f is located 1 bytes to the left of global variable '*.LC1' defined in 'myTestProgram.cpp' (0x401220) of size 3
  '*.LC1' is ascii string 'Hi'

Here we tried to read something of size 1 (probably a char) when we were in anotherFunction. A global-buffer-overflow usually means we tried to access an element past the end of a global array (as opposed to a local array, which causes a stack-buffer-overflow). The global array is either something we declared outside of any function or something the compiler set up for us, usually an array to hold the text of a string literal.

From the two green messages, the bad access is 45 bytes beyond the start of some C string myTestProgram.cpp (18 bytes counting the zero byte) the compiler apparently set up with the name of the source file; that bad access position is also 1 byte before the start of "Hi". Of the two ways of looking at it, it's a lot likelier the incorrect code asked to look one byte before the 'H' of "Hi" than look way past the end of a string our code never even mentions. Indeed, when k is 0, s[k-1] tries to access s[-1], one position before the 'H'.

How about this:

	int main()
	{
	    int* p = nullptr;
	    *p = 42;
	    return *p;
	}

It produces

myBadPtr.cpp:4:5: runtime error: store to null pointer of type 'int'
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1225==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 ...
==1225==The signal is caused by a WRITE memory access.
==1225==Hint: address points to the zero page.
    #0 ... in main ...

Even if it didn't directly tell you about trying to store through a null pointer, you could tell from the fact that you tried to WRITE (as opposed to READ) involving the unknown address 0x000000000000; on most machines, the null pointer is represented by address 0x000000000000 (and the runtime system prevents you from ever actually reading from or storing into that address).

Here's another one:

struct Blah
{
	double x[10];
	double y;
};

int main()
{
	Blah* bp = new Blah;
	delete bp;
	bp->y = 42;
}

It produces

==30268==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000070 ...
WRITE of size 8 at 0x602000000070 ...
    ... in main ...

0x602000000070 is located 80 bytes inside of 88-byte region [0x602000000020,0x602000000078)
    ...

The heap-use-after-free error means you tried to follow a pointer to an object that has already been deleted. The WRITE of size 8 in the code above is consistent with trying to store into a double. The y member of a Blah object comes after the 10-double x member, so is 80 (10*8) bytes from the start of a Blah object.

If the main routine above were instead

int main()
{
	Blah* bp = new Blah;
	delete bp;
	delete bp;
}

we'd get

==30268==ERROR: AddressSanitizer: ==33125==ERROR: AddressSanitizer: attempting double-free on 0x608000000020 ...
    ...
indicating the attempt to delete an object that has already been deleted.