Google Sanitizers

Intro

Valgrind has long been a go-to tool for debugging memory-related issues in C and C++ programs. However, it's not without its drawbacks—primarily performance overhead and lack of integration with build processes. Enter Google Sanitizers, a suite of powerful tools designed to address many of Valgrind's limitations.

Google Sanitizers enable you to embed debugging capabilities directly into your executables or libraries by simply adding compilation flags. These tools provide real-time feedback on undefined behaviors, memory errors, and thread issues with minimal performance impact compared to traditional tools like Valgrind. Let's explore how they work and what they bring to the table.

UBSan - Undefined Behavior Sanitizer

UndefinedBehaviorSanitizer (UBSan) detects undefined behavior in your code during runtime by instrumenting it at compile time. UBSan identifies a wide range of issues, including:
- Array index out-of-bounds access (where bounds can be statically determined).
- Invalid bitwise shifts.
- Dereferencing null or misaligned pointers.
- Signed integer overflows.
- Invalid floating-point operations, such as overflows during conversions.

UBSan is fast and integrates seamlessly into your build process. For the full list of supported checks, visit the official UBSan documentation.

Example: Null Pointer Dereference

Let's see UBSan in action with a simple example:

Code:

int main(void) {
    int *a = 0;
    *a = 0;
    return 0;
}

Compilation Command:

clang -g -fsanitize=undefined -no-pie -fno-pie -O0 test_ubsan.c

Execution:

$ ./a.out
test_ubsan.c:3:5: runtime error: store to null pointer of type 'int'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior test_ubsan.c:3:5 in 
UndefinedBehaviorSanitizer:DEADLYSIGNAL
==15089==ERROR: UndefinedBehaviorSanitizer: SEGV on unknown address 0x000000000000 (pc 0x5590f1f77d7f bp 0x7fff44d2e0a0 sp 0x7fff44d2e080 T15089)
==15089==The signal is caused by a WRITE memory access.
==15089==Hint: address points to the zero page.
    #0 0x5590f1f77d7f in main /tmp/test_ubsan.c:3:8
    #1 0x7f89ef240249  (/lib/x86_64-linux-gnu/libc.so.6+0x27249) (BuildId: 58254ca972028402bc40624f81388d85ec95f70d)
    #2 0x7f89ef240304 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x27304) (BuildId: 58254ca972028402bc40624f81388d85ec95f70d)
    #3 0x5590f1f4f360 in _start (/tmp/a.out+0x5360) (BuildId: 0a8d02e19032c7a5939a0fe8dd355b77079b7a36)

UndefinedBehaviorSanitizer can not provide additional info.
SUMMARY: UndefinedBehaviorSanitizer: SEGV /tmp/test_ubsan.c:3:8 in main
==15089==ABORTING

As shown, UBSan pinpoints the issue (storing to a null pointer) before the program crashes, making debugging straightforward.

AddressSanitizer (ASan)

AddressSanitizer is a memory error detector that excels at identifying:
- Out-of-bounds memory accesses (heap, stack, and global variables).
- Use-after-free or use-after-scope errors.
- Double-free and invalid free operations.
- Memory leaks (optional detection).

With typical execution slowdowns of about 2x, ASan provides detailed error reports with minimal performance impact.

Example: Buffer Overflow

Code:

#include <string.h>

int main(void) {
    char x[10];
    memset(x, 0, 11);
    return 0;
}

Compilation Command:

clang -g -fsanitize=address -no-pie -fno-pie -O0 test_asan.c

Execution:

$ ./a.out
=================================================================
==15164==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffe8af7b46a at pc 0x55e640bf577c bp 0x7ffe8af7b430 sp 0x7ffe8af7ac00
WRITE of size 11 at 0x7ffe8af7b46a thread T0
    #0 0x55e640bf577b in __asan_memset (/tmp/a.out+0xa277b) (BuildId: fd79029e16da7f25c0b713ee5479dc2a76f16f29)
    #1 0x55e640c30f71 in main /tmp/test_asan.c:5:5
    #2 0x7f1ed7bf4249  (/lib/x86_64-linux-gnu/libc.so.6+0x27249) (BuildId: 58254ca972028402bc40624f81388d85ec95f70d)
    #3 0x7f1ed7bf4304 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x27304) (BuildId: 58254ca972028402bc40624f81388d85ec95f70d)
    #4 0x55e640b73300 in _start (/tmp/a.out+0x20300) (BuildId: fd79029e16da7f25c0b713ee5479dc2a76f16f29)

Address 0x7ffe8af7b46a is located in stack of thread T0 at offset 42 in frame
    #0 0x55e640c30eaf in main /tmp/test_asan.c:3

  This frame has 1 object(s):
    [32, 42) 'x' (line 4) <== Memory access at offset 42 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow (/tmp/a.out+0xa277b) (BuildId: fd79029e16da7f25c0b713ee5479dc2a76f16f29) in __asan_memset
Shadow bytes around the buggy address:
  0x1000515e7630: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000515e7640: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000515e7650: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000515e7660: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000515e7670: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x1000515e7680: 00 00 00 00 00 00 00 00 f1 f1 f1 f1 00[02]f3 f3
  0x1000515e7690: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000515e76a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000515e76b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000515e76c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x1000515e76d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
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
==15164==ABORTING

ASan detects that the code writes beyond the bounds of the stack-allocated array x. The error report even specifies the problematic memory location, making it easier to fix.

Example: Memory Leak

Code:

#include <stdlib.h>

int main(void) {
    void *p = malloc(10);
    printf("Allocated: %p\n", p);

    p = NULL;
    return 0;  // No free() is called
}

Compilation Command:

clang -g -fsanitize=leak -no-pie -fno-pie -O0 test_asan.c

Execution:

$ ./a.out
=================================================================
==15193==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 10 byte(s) in 1 object(s) allocated from:
    #0 0x55bbd679714e in __interceptor_malloc (/tmp/a.out+0xa314e) (BuildId: d8417d29f5c2f673cd493eb2e1bd0ea607a9597d)
    #1 0x55bbd67d1eb8 in main /tmp/test_asan.c:4:15
    #2 0x7fa1c2868249  (/lib/x86_64-linux-gnu/libc.so.6+0x27249) (BuildId: 58254ca972028402bc40624f81388d85ec95f70d)

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

MemorySanitizer (MSan)

MemorySanitizer tracks uninitialized memory usage. It instruments the code at compile time and reports runtime issues such as:
- Using uninitialized values in conditionals or calculations.
- Dereferencing uninitialized pointers.
- Passing uninitialized data to libc calls.

MSan introduces a typical slowdown of about 3x and requires code to be built with Clang.

Example: Use of Uninitialized Value

Code:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv) {
  int* a = malloc(sizeof(int)*10);
  a[5] = 0;
  volatile int b = a[argc];
  if (b)
    printf("xx\n");
  free(a);
  return 0;
}

Compilation Command:

clang -g -fsanitize=memory -fsanitize-memory-track-origins=2 -no-pie -fno-pie -O0 test_msan.c

Execution:

$ ./a.out
==14289==WARNING: MemorySanitizer: use-of-uninitialized-value
    #0 0x55575b244741 in main /tmp/test_msan.c:8:7
    #1 0x7f47dbfba249  (/lib/x86_64-linux-gnu/libc.so.6+0x27249) (BuildId: 58254ca972028402bc40624f81388d85ec95f70d)
    #2 0x7f47dbfba304 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x27304) (BuildId: 58254ca972028402bc40624f81388d85ec95f70d)
    #3 0x55575b1be2a0 in _start (/tmp/a.out+0x212a0) (BuildId: 583d937b2eb462a774295379d0c50e80d3bec5d3)

  Uninitialized value was stored to memory at
    #0 0x55575b2446c0 in main /tmp/test_msan.c:7:16
    #1 0x7f47dbfba249  (/lib/x86_64-linux-gnu/libc.so.6+0x27249) (BuildId: 58254ca972028402bc40624f81388d85ec95f70d)

  Uninitialized value was created by a heap allocation
    #0 0x55575b1f2c90 in __interceptor_malloc (/tmp/a.out+0x55c90) (BuildId: 583d937b2eb462a774295379d0c50e80d3bec5d3)
    #1 0x55575b2444a0 in main /tmp/test_msan.c:5:12
    #2 0x7f47dbfba249  (/lib/x86_64-linux-gnu/libc.so.6+0x27249) (BuildId: 58254ca972028402bc40624f81388d85ec95f70d)

SUMMARY: MemorySanitizer: use-of-uninitialized-value /tmp/test_msan.c:8:7 in main
Exiting

MSan clearly indicates where uninitialized memory is used, along with stack traces and variable origins.

Extra: Control sanitizer during runtime

You can fine-tune sanitizer behavior dynamically by setting environment variables before program execution. Each sanitizer has its own set of options:

  • AddressSanitizer, use ASAN_OPTIONS.
  • UndefinedBehaviorSanitizer, use UBSAN_OPTIONS.

For example, to increase verbosity and track more detailed memory allocation contexts with AddressSanitizer, you can run:

ASAN_OPTIONS=verbosity=1:malloc_context_size=20 ./a.out

For a complete list of available runtime options, refer to the official AddressSanitizer runtime flags documentation here.

Extra: List errors

By default, encountering a sanitizer error causes the application to abort and return a non-zero exit code. This behavior is useful for early detection but can be inconvenient if you want to collect multiple errors during a single run.

To allow the program to continue after a sanitizer-detected issue, you can compile with the -fsanitize-recover=all flag. This setting enables recovery for all sanitizer checks, allowing the program to report multiple issues without immediately terminating.

Once program is compiled with -fsanitize-recover=all it has to be run with appropriate flag halt_on_error=1, official documentation is pretty explicit about it:

Even if a recovery mode is turned on the compiler side, it needs to be also enabled on the runtime library side, otherwise the failures are still fatal. The runtime library defaults to halt_on_error=0 for ThreadSanitizer and UndefinedBehaviorSanitizer, while default value for AddressSanitizer is halt_on_error=1. This can be overridden through setting the halt_on_error flag in the corresponding environment variable.

So to fully enable errors recovery you should run:

ASAN_OPTIONS=halt_on_error=0 ./a.out

Extra: Print sanitizers settings

To print all available settings, their values and their descriptions you can use help=1 parameter:

ASAN_OPTIONS=help=1 ./a.out

Extra: DEADLYSIGNAL

If you encounter hanging program with repetitive logs like:

AddressSanitizer:DEADLYSIGNAL
AddressSanitizer:DEADLYSIGNAL
AddressSanitizer:DEADLYSIGNAL
AddressSanitizer:DEADLYSIGNAL
AddressSanitizer:DEADLYSIGNAL
AddressSanitizer:DEADLYSIGNAL

This is likely due to Address Space Layout Randomization (ASLR). Disable ASLR temporarily:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

Remember to re-enable it afterward:

echo 1 | sudo tee /proc/sys/kernel/randomize_va_space

If you cannot change kernel settings, another fix is to disable PIE in the binaries:

clang -g -fsanitize=address -no-pie -fno-pie -O0 test_asan.c

Conclusion

Google Sanitizers offer a compelling alternative to Valgrind by embedding powerful debugging capabilities into your build process. They provide detailed and actionable feedback on undefined behavior, memory errors, and uninitialized variable usage with significantly lower runtime overhead.

By integrating these sanitizers into your development workflow, you can identify and fix issues earlier, improve code quality, and ensure safer software. While Valgrind remains an excellent tool for certain scenarios, the speed and precision of Google Sanitizers make them a must-have for modern C/C++ developers.

Published:

Last updated:

Category: C

Comments

Leave a comment