// HACKER NEWS — CYBERSECURITY
Data races and the limits of ThreadSanitizer in C and Go
Have you ever wondered what a data race is? How a race detector works? If there are bugs in your race detector? We take a look.
You are getting early access to this article as a subscriber. Your support makes articles like this possible. Thank you.
To gain confidence in code we write tests. But when you add concurrency to the mix the bugs become nondeterministic. To gain additional confidence in concurrent code we might enable a race detector that tells us that a race happened in some piece of code during a run. This is useful because our tests might still have passed even though a race happened. The existence of the race, whether our tests otherwise fail or not, means that a latent bug absolutely exists.
Most major language implementations that have any race detector (Clang, GCC, Go, Swift, OCaml) use LLVM’s ThreadSanitizer. ThreadSanitizer (TSan) is not well documented. It has gone through three major iterations and while you can find the algorithm for TSan version two (released in 2012), the author of TSan suggests we just read the source to understand version three (released in 2021). Perhaps someone will contribute new docs.
There is a rich history of algorithms for detecting data races like Eraser (which influenced TSan version one), FastTrack (has ideas in common with TSan versions two and three), RaceTrack by Microsoft, and so on. Each algorithm has its own limitations, as TSan does too. And Clang, GCC, and Go do emit generic entry points for race detector libraries, even if TSan is the only one in serious use today.
In this article we’ll walk through the basics of data races. Then we’ll implement an idealized interpreter in Python for multi-threaded C code alongside FastTrack-style vector clocks to show how TSan roughly works. Then we’ll show how architecture choices make it possible to overload TSan in a few dimensions, causing it to miss obvious data races. For example, TSan cannot reliably report data races while crossing a 255 total thread boundary. This is not a particularly rare situation, considering web services implemented in a language like Go with one goroutine per request.
Some of these issues were discussed in Chapter 6 of Farzam Dorostkar’s 2025 PhD thesis, “Identifying and mitigating implementation-induced data race detection blind spots in ThreadSanitizer v3”. The thesis is partly in French, but Chapter 6 is in English.
There are also a few Go-specific scenarios that go undetected by TSan. We'll cover a design decision in sync.Pool that hides any race between two goroutines whose pooled objects, even in unrelated pools, hash to the same slot.
All of this is not to say TSan is a bad tool or that the authors should have done a better job. I would not be happy without TSan access. Nonetheless, it’s good to know its limitations.
The C11 standard says, "[the] execution of a program contains a data race if it contains two conflicting actions in different threads, at least one of which is not atomic, and neither happens before the other. Any such data race results in undefined behavior." Go says, “[a] data race is defined as a write to a memory location happening concurrently with another read or write to that same location, unless all the accesses involved are atomic data accesses as provided by the sync/atomic package.” A data race is also, at least in theory, something we can automatically detect without additional guidance.