Skip to content

🛡️ Sanitizers Security Scan

Stop chasing bugs manually. Let the compiler catch Use-After-Free, Buffer Overflow, và Data Races at runtime với near-zero cost.

Why Not Valgrind?

┌─────────────────────────────────────────────────────────────────────────┐
│                    VALGRIND vs SANITIZERS                                │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   Tool             Slowdown    Coverage    Production CI                │
│   ────────────     ─────────   ─────────   ─────────────                │
│   Valgrind         20-50x      High        ❌ Too slow                  │
│   AddressSanitizer 2x          High        ✅ Google uses in CI         │
│   ThreadSanitizer  5-15x       Races only  ✅ Used in Chrome CI         │
│   UBSan            <1.5x       UB only     ✅ Always enabled            │
│                                                                         │
│   HPN Policy:                                                           │
│   • ASan: ci-san-asan.yml  → Run on every PR                            │
│   • TSan: ci-san-tsan.yml  → Run on concurrent code changes             │
│   • UBSan: ALWAYS enabled with -fsanitize=undefined                     │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

AddressSanitizer (ASan) — Memory Bugs

What ASan Catches

  • Use-after-free — Dangling pointer access
  • Heap buffer overflow — Out-of-bounds write/read
  • Stack buffer overflow — Local array overflow
  • Use-after-return — Returning pointer to local
  • Double-free — Freeing memory twice
  • Memory leaks — With -fsanitize=leak

Lab: Catching Use-After-Free

cpp
// bug.cpp — A nasty dangling pointer bug
#include <iostream>
#include <memory>

int main() {
    int* ptr = new int(42);
    delete ptr;
    
    // ❌ USE-AFTER-FREE: ptr is dangling!
    std::cout << "Value: " << *ptr << std::endl;
    
    return 0;
}

Compile with ASan

bash
# Enable ASan
g++ -fsanitize=address -g -o bug bug.cpp

# Or with Clang (recommended)
clang++ -fsanitize=address -g -o bug bug.cpp

# Run
./bug

ASan Output (Colorful!)

=================================================================
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 
0x602000000010 at pc 0x000000401234 bp 0x7ffd12345670 sp 0x7ffd12345668

READ of size 4 at 0x602000000010 thread T0
    #0 0x401233 in main bug.cpp:10
    #1 0x7f1234567890 in __libc_start_main
    #2 0x401099 in _start

0x602000000010 is located 0 bytes inside of 4-byte region 
[0x602000000010,0x602000000014)
freed by thread T0 here:
    #0 0x7f12345abcde in operator delete(void*)
    #1 0x401200 in main bug.cpp:6

previously allocated by thread T0 here:
    #0 0x7f12345bcdef in operator new(unsigned long)
    #1 0x4011f0 in main bug.cpp:5

SUMMARY: AddressSanitizer: heap-use-after-free bug.cpp:10 in main
=================================================================
]

💡 READING ASAN OUTPUT

  1. Type of bug: heap-use-after-free
  2. Where accessed: bug.cpp:10
  3. Where freed: bug.cpp:6
  4. Where allocated: bug.cpp:5

ASan tells you the COMPLETE story!


How ASan Works: Shadow Memory

┌─────────────────────────────────────────────────────────────────────────┐
│                    SHADOW MEMORY CONCEPT                                 │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   APPLICATION MEMORY                SHADOW MEMORY (1/8 size)            │
│   ─────────────────────             ────────────────────────            │
│                                                                         │
│   ┌───────────────────┐             ┌───────────────────┐               │
│   │  8 bytes app mem  │ ─────────► │  1 byte shadow   │               │
│   └───────────────────┘             └───────────────────┘               │
│                                                                         │
│   Shadow byte values:                                                   │
│   • 0x00: All 8 bytes accessible                                        │
│   • 0xNN: First N bytes accessible (1-7)                                │
│   • 0xfa: Heap left redzone                                             │
│   • 0xfb: Heap right redzone                                            │
│   • 0xfd: Freed heap memory                                             │
│   • 0xf1: Stack left redzone                                            │
│                                                                         │
│   On every memory access:                                               │
│   1. Compute shadow address: (addr >> 3) + offset                       │
│   2. Check shadow byte                                                  │
│   3. If poisoned → Report error                                         │
│                                                                         │
│   Cost: ~2x slowdown (vs 20x for Valgrind)                              │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

ThreadSanitizer (TSan) — Data Races

What TSan Catches

  • Data race — Two threads access same memory, at least one writes
  • Use-after-destruction — Destroyed mutex/condition_variable
  • Thread leaks — Threads not joined

Lab: Catching Data Race

cpp
// race.cpp
#include <thread>
#include <iostream>

int counter = 0;  // Shared, no synchronization!

void Increment() {
    for (int i = 0; i < 100000; ++i) {
        counter++;  // ❌ DATA RACE!
    }
}

int main() {
    std::thread t1(Increment);
    std::thread t2(Increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Counter: " << counter << std::endl;
    // Expected: 200000, Actual: Random!
}

Compile with TSan

bash
# Cannot combine with ASan!
clang++ -fsanitize=thread -g -o race race.cpp

./race

TSan Output

==================
WARNING: ThreadSanitizer: data race (pid=12345)
  Write of size 4 at 0x000000601040 by thread T2:
    #0 Increment() race.cpp:9
    #1 void std::__invoke_impl<void, void (*)()>...

  Previous write of size 4 at 0x000000601040 by thread T1:
    #0 Increment() race.cpp:9
    #1 void std::__invoke_impl<void, void (*)()>...

  Location is global 'counter' of size 4 at 0x000000601040

SUMMARY: ThreadSanitizer: data race race.cpp:9 in Increment()
==================

The Fix

cpp
#include <atomic>

std::atomic<int> counter{0};  // ✅ Thread-safe!

void Increment() {
    for (int i = 0; i < 100000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

UndefinedBehaviorSanitizer (UBSan)

What UBSan Catches

  • Signed integer overflow
  • Null pointer dereference
  • Misaligned pointer access
  • Out-of-bounds array index
  • Division by zero
bash
clang++ -fsanitize=undefined -g -o ub ub.cpp

# Can combine with ASan!
clang++ -fsanitize=address,undefined -g -o both both.cpp

CMake Integration

cmake
# CMakeLists.txt
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)

if(ENABLE_ASAN)
    message(STATUS "AddressSanitizer enabled")
    add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
    add_link_options(-fsanitize=address)
endif()

if(ENABLE_TSAN)
    message(STATUS "ThreadSanitizer enabled")
    add_compile_options(-fsanitize=thread)
    add_link_options(-fsanitize=thread)
endif()

if(ENABLE_UBSAN)
    message(STATUS "UndefinedBehaviorSanitizer enabled")
    add_compile_options(-fsanitize=undefined)
    add_link_options(-fsanitize=undefined)
endif()

# Usage:
# cmake -B build -DENABLE_ASAN=ON
# cmake --build build

GitHub Actions CI

yaml
# .github/workflows/sanitizers.yml
name: Sanitizers

on: [push, pull_request]

jobs:
  asan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build with ASan
        run: |
          cmake -B build -DENABLE_ASAN=ON
          cmake --build build
      - name: Run Tests
        run: cd build && ctest --output-on-failure

  tsan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build with TSan
        run: |
          cmake -B build -DENABLE_TSAN=ON
          cmake --build build
      - name: Run Tests
        run: cd build && ctest --output-on-failure

Best Practices

┌─────────────────────────────────────────────────────────────────────────┐
│                    SANITIZER BEST PRACTICES                              │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   ✅ DO                                                                  │
│   ─────                                                                 │
│   • Run ASan in CI on every PR                                          │
│   • Use TSan for multithreaded code                                     │
│   • Always enable UBSan (low overhead)                                  │
│   • Compile with -g for readable stack traces                           │
│   • Set ASAN_OPTIONS for more info                                      │
│                                                                         │
│   ❌ DON'T                                                               │
│   ───────                                                               │
│   • Don't combine ASan + TSan (incompatible)                            │
│   • Don't deploy with sanitizers (2-15x slowdown)                       │
│   • Don't ignore sanitizer warnings                                     │
│   • Don't use -O0 with sanitizers (slow + may hide bugs)                │
│                                                                         │
│   🔧 ENVIRONMENT VARIABLES                                              │
│   ─────────────────────────                                             │
│   ASAN_OPTIONS="detect_leaks=1:halt_on_error=0"                         │
│   TSAN_OPTIONS="second_deadlock_stack=1"                                │
│   UBSAN_OPTIONS="print_stacktrace=1"                                    │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Bước tiếp theo

🔬 Profiling → — Valgrind, Linux Perf, FlameGraphs