What does a Thread Sanitizer report look like, how does it report to CI, and why does a clean TSan run not prove the code race free?
Predict first
Two queues increment a global counter. What are the exit statuses under TSan by default and with abort disabled?
// === main.swift ===
import Foundation
nonisolated(unsafe) var counter = 0
let g = DispatchGroup()
for _ in 0..<2 { DispatchQueue.global().async(group: g) { for _ in 0..<1000 { counter += 1 } } }
g.wait()
print("done")
- 134 and 66
- 0 and 0
- 66 and 134
Mental model. A speed camera only catches the cars that pass it; a month without tickets does not prove nobody speeds.
TSan reports two accesses to the same address from different threads without ordering between them. The output names the kind (Swift access race or data race), each access with its stack and thread (a GCD worker thread here), the previous conflicting access, and the location (global 'counter'). The fix is to establish ordering: an actor, a serial queue, a lock or an atomic type, not a sleep. Under Swift 6 language mode many such races are compile errors, so TSan is most valuable for code using nonisolated(unsafe), unsafe pointers, C and Objective-C or older Swift 5 modules.
Exit status: the default run exits 134 (abort), and with TSAN_OPTIONS=abort_on_error=0 it exits 66. TSan detects races on the accesses it observes, so it needs the racy code to run, and detection is not guaranteed: in my runs an unrelated variation of options sometimes produced no report at all for the same program. Treat a clean run as “no race seen on these executions”: run the whole suite, repeat on CI, stress the concurrent paths, and combine with design rules (isolation, Sendable) rather than relying on the tool.
Default aborts; without abort the runtime exits with its race exit code, 66.
Common traps
- Concluding “TSan is clean, so it is safe”. It only sees executed, overlapping accesses.
- Silencing a report with a sleep. It hides the race from the tool, not from users.
They might follow up with
- Can TSan and ASan run together? No (Q458).
- What replaces the lock? An actor or serial queue that owns the state.
Related questions
- 23.3.1 What do the Address, Thread and Undefined Behavior sanitizers catch, what do they cost, and when do you use…
- 23.8.1 What does a failing Address Sanitizer run look like to CI: exit status, ASAN_OPTIONS, and how do you read the…
← What does a failing Address Sanitizer run look like to CI: exit… Which runtime checks fire when you misuse continuations… →