4 min read
On this page

Sanitizers

Sanitizers are compiler-instrumented runtime checks that detect bugs that would otherwise cause silent corruption, crashes hours later, or security vulnerabilities. They add code to your program at compile time that checks for undefined behavior, memory errors, and data races as the program runs. Sanitizers find bugs that debuggers and testing alone miss. They are the most effective tools available for catching C bugs early.

AddressSanitizer (ASan)

AddressSanitizer detects memory errors: buffer overflows, use-after-free, use-after-return, double free, and memory leaks. Enable it with a single compiler flag.

gcc -fsanitize=address -g -O1 program.c -o program
./program

Use -O1 rather than -O0 with ASan. It needs some optimization for accuracy but full optimization can interfere with detection.

Buffer Overflow Detection

#include <stdio.h>

int main(void) {
    int arr[5] = {1, 2, 3, 4, 5};
    /* Bug: accessing arr[5] is out of bounds */
    printf("%d\n", arr[5]);
    return 0;
}
=================================================================
==12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd12345674
READ of size 4 at 0x7ffd12345674 thread T0
    #0 0x555555555189 in main /home/user/program.c:6
    #1 0x7f1234567890 in __libc_start_main

Address 0x7ffd12345674 is located in stack of thread T0 at offset 52 in frame
    #0 0x555555555140 in main /home/user/program.c:3

  This frame has 1 object(s):
    [32, 52) 'arr' (line 4) <== Memory access at offset 52 overflows this variable

ASan tells you exactly what happened (stack buffer overflow), where it happened (file and line), and which variable was overflowed (arr). Without ASan, this might silently corrupt adjacent stack variables and crash much later.

Use-After-Free Detection

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

int main(void) {
    int *p = malloc(sizeof(int));
    *p = 42;
    free(p);
    /* Bug: using freed memory */
    printf("%d\n", *p);
    return 0;
}
=================================================================
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010
READ of size 4 at 0x602000000010 thread T0
    #0 0x555555555189 in main /home/user/program.c:9

0x602000000010 is located 0 bytes inside of 4-byte region
freed by thread T0 here:
    #0 0x7f1234567890 in __interceptor_free
    #1 0x555555555170 in main /home/user/program.c:7

previously allocated by thread T0 here:
    #0 0x7f1234567abc in __interceptor_malloc
    #1 0x555555555150 in main /home/user/program.c:5

ASan shows three stack traces: where the bad access happened, where the memory was freed, and where it was originally allocated. This is invaluable for tracking use-after-free bugs in large programs.

Double Free Detection

#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof(int));
    free(p);
    free(p); /* Bug: double free */
    return 0;
}
==12345==ERROR: AddressSanitizer: attempting double-free on 0x602000000010
    #0 0x7f1234567890 in __interceptor_free
    #1 0x555555555160 in main /home/user/program.c:6

Memory Leak Detection

ASan can also detect memory leaks. Set the environment variable:

ASAN_OPTIONS=detect_leaks=1 ./program
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 40 bytes in 1 object(s) allocated from:
    #0 0x7f1234567abc in __interceptor_malloc
    #1 0x555555555150 in main /home/user/program.c:5

UndefinedBehaviorSanitizer (UBSan)

UndefinedBehaviorSanitizer detects operations that the C standard says have undefined behavior. These bugs are particularly dangerous because they may appear to work on one compiler or optimization level but break silently on another.

gcc -fsanitize=undefined -g program.c -o program

Signed Integer Overflow

#include <limits.h>
#include <stdio.h>

int main(void) {
    int x = INT_MAX;
    /* Bug: signed overflow is undefined behavior */
    int y = x + 1;
    printf("%d\n", y);
    return 0;
}
program.c:6:17: runtime error: signed integer overflow:
2147483647 + 1 cannot be represented in type 'int'

Without UBSan, this might wrap around, produce zero, or cause the optimizer to make incorrect assumptions about your code. Signed overflow is undefined behavior and compilers exploit it for optimization.

Null Pointer Dereference

#include <stdlib.h>

int main(void) {
    int *p = NULL;
    *p = 42; /* Bug: null dereference */
    return 0;
}
program.c:5:5: runtime error: store to null pointer of type 'int'

Shift Overflow

#include <stdio.h>

int main(void) {
    int x = 1;
    /* Bug: shifting by 32 or more is undefined for a 32-bit int */
    int y = x << 32;
    printf("%d\n", y);
    return 0;
}
program.c:5:17: runtime error: shift exponent 32 is too large
for 32-bit type 'int'

ThreadSanitizer (TSan)

ThreadSanitizer detects data races — when two threads access the same memory location concurrently and at least one access is a write, without synchronization.

gcc -fsanitize=thread -g program.c -o program -lpthread
#include <pthread.h>
#include <stdio.h>

int counter = 0; /* Shared state, no protection */

void *increment(void *arg) {
    (void)arg;
    for (int i = 0; i < 100000; i++) {
        counter++; /* Bug: data race */
    }
    return NULL;
}

int main(void) {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, increment, NULL);
    pthread_create(&t2, NULL, increment, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("Counter: %d\n", counter);
    return 0;
}
==================
WARNING: ThreadSanitizer: data race (pid=12345)
  Write of size 4 at 0x555555558010 by thread T2:
    #0 increment /home/user/program.c:9 (program+0x1234)

  Previous write of size 4 at 0x555555558010 by thread T1:
    #0 increment /home/user/program.c:9 (program+0x1234)

  Location is global 'counter' of size 4 at 0x555555558010
==================

TSan identifies the exact variable, the two conflicting accesses, and the threads involved.

Combining Sanitizers

ASan and UBSan can be combined in a single build:

gcc -fsanitize=address,undefined -g -O1 program.c -o program

TSan cannot be combined with ASan — they use incompatible memory instrumentation. Run them as separate builds:

# Build 1: memory errors and undefined behavior
gcc -fsanitize=address,undefined -g -O1 program.c -o program_asan

# Build 2: data races
gcc -fsanitize=thread -g -O1 program.c -o program_tsan -lpthread

Compile-Time Warnings

Warnings are the first line of defense. They catch many bugs at compile time with zero runtime cost.

gcc -Wall -Wextra -Wpedantic -Werror program.c -o program
  • -Wall enables most common warnings: unused variables, implicit declarations, format string mismatches
  • -Wextra enables additional warnings: unused parameters, sign comparison issues
  • -Wpedantic enforces strict C standard compliance
  • -Werror treats warnings as errors so they cannot be ignored
#include <stdio.h>

int process(int x, int y) {
    /* -Wextra catches: parameter 'y' unused */
    if (x = 5) { /* -Wall catches: suggest parentheses around assignment used as truth value */
        return x;
    }
    /* -Wall catches: control reaches end of non-void function */
}

Every one of these warnings indicates a real bug or a likely mistake.

Static Analyzers

Static analyzers examine source code without running it. They find deeper issues than compiler warnings.

cppcheck

cppcheck --enable=all --std=c17 src/

cppcheck finds null pointer dereferences, buffer overflows, memory leaks, dead code, and style issues. It has few false positives, making it practical for regular use.

Compiler Built-in Analyzers

GCC and Clang include static analysis capabilities:

gcc -fanalyzer program.c -o program

GCC's -fanalyzer flag performs interprocedural analysis, tracking values across function calls to find use-after-free, double-free, null dereferences, and resource leaks.

Defense in Layers

No single tool catches every bug. Effective C development uses multiple tools together.

# Layer 1: Compile-time warnings (every build)
CFLAGS = -Wall -Wextra -Wpedantic -Werror

# Layer 2: Static analysis (CI pipeline)
cppcheck --enable=all src/

# Layer 3: ASan + UBSan (test suite)
gcc -fsanitize=address,undefined -g -O1 $(SRCS) -o test_asan
./test_asan

# Layer 4: TSan (concurrent code tests)
gcc -fsanitize=thread -g -O1 $(SRCS) -o test_tsan -lpthread
./test_tsan

# Layer 5: Valgrind (comprehensive memory analysis)
valgrind --leak-check=full ./program

Each layer catches different classes of bugs. Warnings cost nothing. Sanitizers add 2-5x runtime overhead during testing. Static analyzers run in CI. Together, they catch the vast majority of C bugs before they reach production.

Common Pitfalls

  • Not using sanitizers during testing — Sanitizers should be part of your CI pipeline. A test suite that passes without sanitizers may be hiding memory corruption, undefined behavior, and data races.
  • Ignoring sanitizer output — ASan and UBSan output looks verbose, but every report is a real bug. There are no false positives with ASan. Read the full stack traces.
  • Running ASan and TSan together — They cannot be combined. Build and test separately.
  • Suppressing warnings instead of fixing them — Casting to (void) to suppress an unused parameter warning is fine. Adding -Wno-* flags to suppress substantive warnings is hiding bugs.
  • Only using sanitizers in debug builds — Sanitizers work with optimization. -O1 is recommended. Some bugs only manifest with optimization because the compiler exploits undefined behavior differently.
  • Treating sanitizers as optional — In C, undefined behavior is not hypothetical. It is the source of real security vulnerabilities. Sanitizers are not optional tooling — they are essential practice.

Key Takeaways

  • AddressSanitizer (-fsanitize=address) detects buffer overflows, use-after-free, double free, and memory leaks with exact stack traces showing where memory was allocated and freed.
  • UndefinedBehaviorSanitizer (-fsanitize=undefined) catches signed overflow, null dereference, shift errors, and other undefined behavior that silently corrupts results.
  • ThreadSanitizer (-fsanitize=thread) detects data races in multithreaded programs.
  • Compile with -Wall -Wextra -Wpedantic on every build. These warnings catch bugs at zero runtime cost.
  • Static analyzers like cppcheck and GCC's -fanalyzer find interprocedural bugs without running the program.
  • Defense in layers: warnings, static analysis, ASan+UBSan, TSan, and Valgrind each catch different classes of bugs. Use all of them.