How ThreadSanitizer Detects Data Races in C

ThreadSanitizer (TSan) is a dynamic analysis tool integrated into modern compilers like Clang and GCC that detects data races in C applications at runtime. It achieves this by instrumenting memory accesses and synchronization events during compilation, and then tracking execution states using shadow memory and vector clocks at runtime. When handling asynchronous execution contexts—such as POSIX threads, work-stealing queues, signal handlers, or event-driven task schedulers—TSan dynamically constructs a directed acyclic graph of "happens-before" relationships. By comparing incoming memory accesses against historical access states, TSan reliably identifies concurrent, conflicting accesses to identical memory locations where at least one operation is a write and no synchronization ordering is enforced.

Compiler Instrumentation and Memory Interception

During the compilation of C code with -fsanitize=thread, the compiler inserts hooks around every load and store instruction. Additionally, TSan intercepts standard library calls, system calls, and synchronization APIs, such as pthread_create, pthread_join, pthread_mutex_lock, pthread_mutex_unlock, and C11 atomic operations.

Whenever an execution context attempts to read from or write to a memory address, the intercepted code executes a runtime check before the actual memory operation occurs. This runtime layer captures the identity of the running context, the memory address, the size of the access, whether the access is a read or a write, and the current logical timestamp of that context.

Shadow Memory Architecture

TSan maps application memory to an internal structure called shadow memory. For every 8 bytes of application memory, TSan reserves a corresponding block of shadow memory cells (typically four to eight shadow words).

Each shadow word stores metadata regarding a past memory access:

  • Thread/Context Identifier: Which execution unit performed the operation.
  • Clock Epoch: The scalar timestamp representing the logical time of the access within that context.
  • Access Attributes: Whether the operation was a read or a write, along with the specific byte offset and range.

When an address is accessed, TSan reads the shadow words associated with that memory block to inspect its recent access history.

Vector Clocks and the Happens-Before Relationship

To determine whether two actions in separate asynchronous contexts occur concurrently, TSan relies on Leslie Lamport's happens-before relation, implemented via vector clocks.

A vector clock is an array of logical timestamps maintained by every thread or asynchronous context, where each element represents the latest known state of all other contexts. Vector clocks evolve based on specific synchronization boundaries:

  1. Thread Spawning: When a thread creates an asynchronous task or child thread, it updates its clock and passes that timestamp snapshot to the child. The child’s initial state establishes a happens-before link with the parent.
  2. Thread Completion: A thread join operation transfers the terminating context's vector clock to the joining thread.
  3. Mutual Exclusion: Releasing a lock increments the releasing context's clock and stores the vector state in the lock object. Acquiring the lock subsequently merges the stored vector clock into the acquiring context's clock via a component-wise maximum operation.
  4. Atomics and Fences: C11 atomics utilizing acquire-release semantics propagate vector clock states between releasing stores and acquiring loads across asynchronous workers.

Race Detection Logic Across Asynchronous Contexts

When an asynchronous context performs a memory operation, TSan compares the context's current vector clock against the timestamps recorded in the target address's shadow memory words.

A data race is identified when all of the following conditions are met:

  1. The incoming access and a recorded shadow access target overlapping memory bytes.
  2. At least one of the two accesses is a write.
  3. The two operations originate from distinct contexts or disjoint asynchronous frames.
  4. The vector clock of the current context indicates that the previous access did not happen-before the current access (meaning the operations are logically concurrent).

If these criteria are satisfied, TSan outputs a diagnostic report detailing the conflicting memory address, the stack trace of the current operation, and the stack trace of the prior conflicting operation stored in the shadow cell.

Non-Standard Asynchronous Execution Contexts

In complex C architectures—such as custom coroutines, fiber libraries, state-machine event loops, or signal-driven operations—synchronization often occurs through user-space signaling rather than standard POSIX primitives.

Because TSan operates at the machine-instruction and standard-library level, user-space context switches that bypass OS primitives can lead to false positives if the causal links are invisible to the runtime. To accurately track these asynchronous contexts, TSan provides an explicit dynamic annotations API. Functions such as __tsan_acquire and __tsan_release allow developers to manually register happens-before edges across custom task dispatchers, work queues, and event loops, ensuring that shadow memory evaluations reflect the true causality of asynchronous code.