Trap Every Crash Before You Fix It
Never fix a crash without trapping it first. In native code, a crash, memory leak, or data race is not gone when the symptom disappears — it comes back the next time someone (or some coding agent) touches that code path, unless something in the build guards against it. The guard is a failing regression test, written before the fix, and run under a sanitizer forever after.
The rule: test first, then fix
- Reproduce the bug as a failing test. Tell your coding agent: "write a regression test that reproduces this crash — do not fix anything yet." Run it. Watch it fail. A test you never saw fail proves nothing.
- Fix the bug. Now the same test flips to green, and you know it flipped because of your fix — not because the repro was wrong.
- Run the test under a sanitizer in CI. A sanitizer-instrumented build turns silent corruption into a loud, precisely-located failure, so the same class of bug can't sneak back in a form your assertions don't check.
This is exactly the loop where coding agents shine: the failing test is an unambiguous target, and "make this test pass without deleting it" is a prompt that's hard to game.
Which sanitizer traps which bug
| Bug class | Sanitizer | Compile flag | Typical cost |
|---|---|---|---|
| Crash / memory corruption (use-after-free, buffer overflow, double-free) | AddressSanitizer (ASan) | -fsanitize=address | ~2x slowdown |
| Memory leak | LeakSanitizer (LSan) | -fsanitize=leak (on by default with ASan on supported platforms) | Near zero until a leak-check pass at process exit |
| Data race | ThreadSanitizer (TSan) | -fsanitize=thread (add -O1 -g for usable output) | ~5x–15x slowdown, ~5x–10x memory |
| Undefined behavior (signed overflow, null deref, out-of-bounds subscript) | UndefinedBehaviorSanitizer (UBSan) | -fsanitize=undefined | Low |
On Windows, MSVC ships ASan since Visual Studio 2019 16.9: compile with /fsanitize=address — works at any optimization level on x86/x64.
Why sanitizer coverage beats line coverage
Line coverage tells you a statement executed. It says nothing about whether that execution scribbled past the end of a buffer, read freed memory, or raced another thread — the program keeps running and the test stays green. A sanitizer checks every memory access and synchronization event during those same test runs, so the identical test suite catches an entire class of bugs that line coverage is structurally blind to. Your regression tests are only as strong as the build they run under: run the suite once normally, and again under ASan/UBSan (and TSan for concurrent code).
Power-user moves
- Separate CI jobs per sanitizer. ASan and TSan can't share one binary — build the test suite once per sanitizer. ASan+UBSan can be combined (
-fsanitize=address,undefined). - Tune the runtime with
ASAN_OPTIONS. Runtime behavior is an environment variable, e.g.ASAN_OPTIONS=halt_on_error=0to keep going past the first report, ordetect_leaks=1/0to toggle leak checking. - Standalone leak checks are almost free. LSan adds nearly no overhead until process exit, so
-fsanitize=leakis cheap enough for every CI run even when a full ASan job is too slow. - On Windows, save the crash as a dump. Set
ASAN_SAVE_DUMPS=MyFileName.dmpand MSVC's ASan writes a crash dump you can replay in the Visual Studio debugger on another machine. - Name regression tests after the bug.
test_issue_1423_use_after_freeturns your test suite into a searchable history of every trap ever sprung — and tells the next agent exactly what must never regress.
Resources
- AddressSanitizer — Clang documentation
- ThreadSanitizer — Clang documentation
- LeakSanitizer — Clang documentation
- UndefinedBehaviorSanitizer — Clang documentation
- AddressSanitizer runtime flags (
ASAN_OPTIONS) — google/sanitizers wiki - AddressSanitizer for MSVC (
/fsanitize=address) — Microsoft Learn
Building an AI feature? Yeda AI designs, audits, and ships production LLM systems.