CWE-366: Race Condition within a Thread
Overview
CWE-366 is the in-process case: two or more threads of one program read and write the same variable, object or collection without synchronizing, so one thread can observe the state mid-update or lose an update to another. The name is confusing - the race is between threads, not within one - and MITRE's description is the clearer statement of it: "If two threads of execution use a resource simultaneously, there exists the possibility that resources may be used while invalid, in turn making the state of execution undefined."
That scope is what decides the fix. The shared thing lives in one address space, so an in-process lock or an atomic type closes the window, and there is no need for a database constraint, a lease or a filesystem primitive.
Relationship to Other CWEs
- CWE-366 (this page) - two or more threads of one process reading and writing the same shared state without synchronizing, so one of them can observe that state mid-update or lose an update to another.
- CWE-362 (Race Condition) - the parent and the broader weakness. It also covers races between processes, servers or replicas, where the shared resource is a database row or a file and an in-process lock cannot reach it. Use CWE-362 when the racing parties are not threads of one program.
- CWE-367 (Time-of-check Time-of-use) - the check-then-use shape, and the right entry for the filesystem cases: a path checked with
stat()and then opened, a file tested for existence and then created. Those races are usually between processes, so they need an atomic filesystem operation rather than a lock. - CWE-364 (Signal Handler Race Condition) - looks like this one and takes the opposite fix. The interrupting code there runs on the same thread, so a mutex deadlocks rather than protects; blocking signals is what works.
- CWE-557 (Concurrency Issues) - the MITRE category all of these sit in. It is not usable as a mapping target itself - prefer this page or one of the above when the finding names a specific shape.
Risk
High: A thread that acts on state which changed after it read it loses an update - two withdrawals settled against one balance, two orders against the last unit of stock - or acts on a security decision that a concurrent change has already invalidated, such as a privilege check that passed just before the privilege was revoked. Other threads can also observe an object part-way through an update, in a state the code never meant to be visible.
Remediation Steps
Core Principle: Concurrency must not break security invariants; synchronize access to shared state and make operations atomic.
Locate the race condition
- Find the code the finding points at: the file, the line and the access to a shared resource that no lock or atomic covers
- Identify the shared resource: a static or instance field, a cached object, a collection, a counter
- Confirm the racing parties are threads of one process. If they are separate processes, replicas or requests hitting a database, the fix is not on this page - see CWE-362, and CWE-367 for the filesystem cases
- Determine the race window: where check and use are separated, or where multiple threads can interleave
- Trace every thread that reaches the shared resource, including the ones a framework creates on your behalf - a servlet container, a thread pool, an async callback, a scheduled task
Use proper synchronization (Primary Defense)
- Use locks/mutexes for critical sections:
synchronized (lock) { }or aReentrantLock(Java),with lock:(Python),std::lock_guardover astd::mutex(C++) - Atomic operations for simple updates:
AtomicInteger.incrementAndGet()(Java),std::atomic<int>(C++) for simple increment/decrement - Thread-safe data structures:
ConcurrentHashMap(Java),queue.Queue(Python), thread-safe collections instead of regular collections - Guard every access with the same lock, reads included. A read left outside the lock can still observe a half-finished update, so protecting only the writes leaves the weakness in place
- Hold one lock across the whole invariant. Two correctly synchronized calls in sequence are not a synchronized pair:
if (map.containsKey(k)) map.get(k)races even on aConcurrentHashMap, which is why those types offer compound operations such ascomputeIfAbsentandputIfAbsent
Close the gap between the check and the action
- Combine check and use in one atomic operation: compare-and-swap rather than a separate read and write
- Hold the lock from check through use: acquire before the check, release after the action, never in between
- Re-check after acquiring the lock: the condition you tested to decide you needed the lock may no longer hold once you have it
- Where the state is not in memory, use the resource's own atomic primitive - a unique constraint or conditional update in a database,
O_CREAT | O_EXCLon a file. See CWE-367; a language-level lock does not serialize anything outside its own process
Use atomic operations
- Compare-and-swap:
compareAndSet(expected, new)for lock-free updates - Increment and decrement:
AtomicInteger.incrementAndGet(),std::atomic::fetch_add() AtomicIntegerandAtomicReferencein Java, andstd::atomicin C++, cover lock-free counters, flags and references
Design thread-safe code
- Keep shared mutable state to a minimum. An immutable object cannot change after construction, so it needs no synchronization at all
- Keep per-thread data in thread-local storage:
ThreadLocal(Java),threading.local()(Python) - Use concurrent data structures where lock contention is a performance problem
Test the race condition fix
- Assert the invariant rather than the mechanism. A counter incremented once by each of N threads must read exactly N at the end; a "lock is held" check passes whether or not the lock covers the whole invariant
- Run the racing operations concurrently and repeatedly - thousands of iterations, threads started from a barrier so they contend rather than run in sequence. A single pass almost always passes
- Use a race detector suited to the runtime: ThreadSanitizer (
-fsanitize=thread) for C, C++, Go and Rust, Valgrind's Helgrind or DRD for C and C++,go test -racefor Go, and jcstress for JVM code, which drives the interleavings a stress loop reaches by luck. Java has no-XX:flag that detects data races - Test under load, and on more than one core - a race that needs true parallelism will not appear on a single-CPU container
- Re-scan with the security scanner to confirm the finding is resolved
Common Vulnerable Patterns
// VULNERABLE - pseudo-code: every pair below reads a value, then acts on it
// as though nothing could have changed in between
if (balance >= amount) { // Check
balance -= amount; // Use - RACE!
}
if (file.exists()) { // Check
file.open(); // Use - RACE!
}
if (isAdmin(user)) { // Check
doAdminAction(); // Use - RACE!
}
Why this is vulnerable: between the two statements the real value can change, so each decision is made against a snapshot that has already expired by the time it is acted on. Nothing in the source marks the gap, which is why the code reads as correct to anyone tracing it one thread at a time.
The three sinks fail differently and the third is the one that surprises. The balance case loses an update: both callers read the same figure, both pass, and one write overwrites the other. The file case has its object substituted underneath it, so the check and the open apply to two different things (CWE-367). The authorization case is a stale decision - isAdmin was true when it was asked, a concurrent revocation lands, and the action proceeds on an answer that is no longer true, which is a race that nobody thinks to look for because the code contains no shared counter and no file.
What also hides all three is that none of them involves malformed input. Every request is well-formed, authenticated and individually legitimate; the attack is entirely in the timing, so input validation, payload inspection and rate limits see nothing to object to.