C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
195: Using Sanitizers (UBSan, ASan, TSan) Together
A few years back, I was mentoring a dev who was losing his mind over a flaky crash in a high-frequency trading module. He had a classic "Heisenbug"—it happened once every thousand runs, usually in production, but never on his machine. He knew he had a concurrency issue, so he turned on ThreadSanitizer (TSan). Then he suspected a buffer overflow, so he tried to add AddressSanitizer (ASan) to the same build. He spent three hours fighting the compiler, wondering why -fsanitize=address,thread was throwing a fit and refusing to link. He felt like he was being punished for wanting more safety.
The reality is that sanitizers aren't just simple flags you toggle on like "dark mode" in an IDE. They are heavy-duty instrumentation tools that fundamentally rewrite how your program interacts with memory and the CPU. While we'd love a single "God Mode" binary that catches everything, the internals of these tools often collide.
The Compatibility Wall
Here is the hard truth: you cannot run ASan and TSan in the same executable. I've seen plenty of engineers try to force it, but they're fighting a losing battle. ASan works by "poisoning" memory around your allocations to catch overflows, while TSan tracks every single memory access to detect data races. Both tools need to intercept malloc and free, and both need to insert "shadow memory" checks into your code. When you try to use both, they fight over the same shadow memory regions and the same function hooks. The result is usually a linker error or a binary that crashes the moment it starts.
UndefinedBehaviorSanitizer (UBSan), however, is the friendly neighbor of the group. Because it focuses on logical violations—like signed integer overflow or null pointer dereferences—rather than memory layout or thread interleaving, it generally plays well with others. You can usually pair UBSan with ASan or TSan without a problem. If I'm hunting a memory leak, I'll run ASan + UBSan. If I'm hunting a race condition, I'll run TSan + UBSan.
Orchestrating a Sanitizer Workflow
Since you can't have one "super-binary," you have to change how you think about your build process. I stop thinking about "the build" and start thinking about "build profiles." In a professional C environment, you don't just have debug and release; you have a matrix of sanitized builds.
I typically set up my Makefile or CMake configuration to handle this via variables. For example, I might have three distinct targets:
# Build for memory and UB checks
gcc -fsanitize=address,undefined -g -O1 main.c -o app_asan
# Build for threading and UB checks
gcc -fsanitize=thread,undefined -g -O1 main.c -o app_tsan
# Standard debug build
gcc -g -O0 main.c -o app_debug
The trick is to integrate these into your Continuous Integration (CI) pipeline. I don't run all of them on every single save—that's overkill and slows down the feedback loop. Instead, I run the ASan build on every PR, and the TSan build on a nightly schedule since race conditions often require more iterations to trigger. If you're working locally, just swap the binary you're executing based on what you're hunting. If the program is crashing with a SIGSEGV, reach for the ASan binary. If it's behaving erratically under load, switch to the TSan binary.
One last tip: keep your optimization level low (-O1 is usually the sweet spot). If you go to -O3, the compiler might optimize away the very patterns the sanitizers are looking for, or it might make the stack traces provided by the sanitizers almost impossible to read. Trust me, there is nothing worse than a TSan report that tells you a race occurred in a function that the compiler inlined into five other places.
📋 Practical Task
Debugging the Corrupt Thread-Safe Queue
You have been handed a piece of code implementing a basic producer-consumer queue. The previous developer claimed it was "thread-safe," but it's crashing sporadically in production. There are two distinct bugs: one is a data race on the queue index, and the other is a subtle buffer overflow when the queue wraps around.
Your Task:
- Create two separate build commands: one using
-fsanitize=address,undefinedand one using-fsanitize=thread,undefined. - Run the ASan build to identify and fix the memory corruption bug.
- Run the TSan build to identify and fix the race condition.
- Verify that both sanitizers report a clean bill of health.
# Buggy Queue Implementation
# queue.c
#include <stdio.h><
#include <stdlib.h><
#include <pthread.h><
#include <unistd.h><
#define SIZE 10
typedef struct {
int buffer[SIZE];
int head;
int tail;
pthread_mutex_t lock;
} Queue;
void produce(Queue *q, int val) {
// BUG 1: Potential race condition here (intentionally missing lock)
q->buffer[q->tail] = val;
// BUG 2: Off-by-one error in wrapping logic
q->tail = (q->tail + 1) % (SIZE + 1);
pthread_mutex_lock(&q->lock);
pthread_mutex_unlock(&q->lock);
}
void* worker(void* arg) {
Queue *q = (Queue*)arg;
for(int i = 0; i < 100; i++) {
produce(q, i);
}
return NULL;
}
int main() {
Queue q = { .head = 0, .tail = 0, .lock = PTHREAD_MUTEX_INITIALIZER };
pthread_t t1, t2;
pthread_create(&t1, NULL, worker, &q);
pthread_create(&t2, NULL, worker, &q);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Finished successfully\n");
return 0;
}There are no comments for now.