CWE-362, Concurrent Execution using Shared Resource with Improper Synchronization, is the entry for race conditions. MITRE describes it as a product with a concurrent code sequence that needs temporary, exclusive access to a shared resource, where a timing window exists in which another code sequence running at the same time can modify that resource.
It applies to multithreaded programs, programs that run several processes against the same files or data, kernels, and services handling many requests at once. MITRE lists C, C++, Java, mobile and ICS/OT platforms.
How it happens
MITRE defines a race condition through two properties. Exclusivity means the code sequence has exclusive access to the resource, so no one else can change it before the sequence finishes. Atomicity means the sequence behaves as one unit, so no other thread or process can run the same instructions against the same resource at the same time.
A race exists when an interfering code sequence can still reach the resource, which breaks exclusivity. The interfering code can be trusted or untrusted, and the attacker can be the one scheduling it.
The reason this keeps happening is that code looks sequential on the screen. A check, then an update. Two lines. But between them, another thread can run. MITRE warns about an innocent looking case: "x++" appears atomic in source code, but at instruction level it is a read, a computation and a write, which are separate steps.
The observed examples show how serious the results get. CVE-2024-38106 describes an operating system kernel that stores sensitive data in improperly locked memory, so local users can gain privileges by winning a race condition. CVE-2022-29527 describes a Go application for cloud management that creates a world writable sudoers file, which lets local attackers inject sudo rules and escalate privileges. CVE-2021-1782 is a chain: improper locking leads to a race condition, as exploited in the wild per CISA KEV.
What an attacker gains
MITRE lists several consequences.
- Denial of service through resource exhaustion, when a race bypasses a cleanup routine or triggers multiple initializations.
- Denial of service through crash, exit or restart, when multiple control flows reach a resource at once and push the product into unexpected states.
- Reading files or application data, when a race is combined with predictable resource names and loose permissions.
- Executing unauthorized code, gaining privileges or bypassing protection, when the unsynchronized code is security critical, such as code that records whether a user is authenticated or modifies important state.
The common thread is that the attacker does not need a flaw in the logic. They need to win the timing, and they can often try thousands of times.
How to find it
MITRE lists several approaches.
- Black box testing can sometimes show evidence through many simultaneous connections that make the software unstable or crash, though this is hard to do reliably.
- White box analysis can spot common idioms, such as time-of-check time-of-use file operations (CWE-367) or double checked locking (CWE-609).
- Automated dynamic analysis with large test suites and diverse inputs, including fuzzing, can surface races.
Practical additions: run your tests under a race detector where your language has one, such as Go's built in one or thread sanitizers for C and C++. Stress test the critical paths with many parallel callers.
How to fix it
MITRE lists many mitigations. In plain steps:
- Use synchronization primitives where the language supports them, and wrap them around only the critical code to limit performance cost.
- Use thread safe capabilities, such as the data access abstraction in Spring.
- Minimize shared resources. Less sharing means less synchronization and fewer unexpected conditions.
- Use thread safe functions and atomic operations on shared variables.
- Use a mutex if available, and avoid related weaknesses such as CWE-412.
- Avoid double checked locking and other mistakes that come from trying to dodge the cost of synchronization.
- Run with the lowest privileges that the task needs, so a successful attack gains little.
A short Python example. The vulnerable withdrawal checks, then subtracts, with a gap between them:
def withdraw(acct, amount):
if acct.balance >= amount: # check
acct.balance -= amount # use: another thread may have run in between
return True
return False
Two threads can both pass the check before either subtracts, so the account goes negative. The fix makes check and update one protected unit:
import threading
class Account:
def __init__(self, balance):
self.balance = balance
self.lock = threading.Lock()
def withdraw(acct, amount):
with acct.lock:
if acct.balance >= amount:
acct.balance -= amount
return True
return False
Where a database holds the balance, the same principle applies: use a transaction or a conditional update so the check and the change happen atomically in one statement.
Try it yourself
Static analysis finds many locking idioms. The demo shows how a code scan reports findings and how you can read them.
Click through it at your own pace, or open it full screen.
Quick check
Why is "x++" a risk in threaded code?
It looks like one operation but is a read, a computation and a write at instruction level, so another thread can run in the middle.
Why does less shared state help?
Fewer shared resources mean fewer places that need synchronization, less complexity in control flow, and fewer unexpected conditions.
Can a race condition be a security flaw and not just a bug?
Yes. MITRE lists privilege escalation, bypass of protection mechanisms and unauthorized code execution when the unsynchronized code is security critical, as in CVE-2024-38106.
Sources
- MITRE CWE-362: https://cwe.mitre.org/data/definitions/362.html