CWE-366, Race Condition within a Thread, covers two threads of execution using a resource at the same time. The resource may be used while it is in an invalid state, and the state of execution becomes undefined.
It applies to C, C++, Java and C#, according to the MITRE record, and really to any threaded program that shares data.
How it happens
A shared object is read by one thread while another changes it. Or one thread frees or tears down a resource while a second still holds a reference to it. Neither thread is wrong in isolation. The bug is in the overlap.
The observed example is CVE-2022-2621, a chain in which two threads in a web browser use the same resource, but one of them can destroy the resource before the other is done with it.
MITRE sums up the core problem this way: if a lock is overcome, data could be altered in a bad state.
What an attacker gains
MITRE lists integrity impact: altered execution logic and an unexpected state. In practice, a reproducible race can turn into corrupted data or a crash, and in memory unsafe languages a use after free can be worse. An attacker who can trigger the timing repeatedly has the advantage, since they only need to win occasionally.
How to find it
MITRE notes static analysis can find some instances of this weakness. Add these checks.
- List every piece of mutable state reachable from more than one thread.
- For each one, name the lock that guards it. If you cannot, that is a finding.
- Run tests under thread sanitizers where your toolchain supports them, and stress test with many concurrent callers.
How to fix it
MITRE's recommended solution is locking. Use a locking mechanism around code that alters or reads persistent data in a multithreaded environment. If no locking mechanism exists, create resource locking validation checks, using flags and signals to enforce your own blocking while another thread uses the resource.
A Java example. The vulnerable version lets two threads interleave:
class Cache {
private Map<String, String> map = new HashMap<>();
void put(String k, String v) { map.put(k, v); } // not safe
String get(String k) { return map.get(k); }
}
A guarded version serializes access:
class Cache {
private final Map<String, String> map = new HashMap<>();
synchronized void put(String k, String v) { map.put(k, v); }
synchronized String get(String k) { return map.get(k); }
}
For heavier use, a concurrent collection built for the purpose is a better choice. The principle is the same: every access to shared state goes through one guard.
Quick check
What does MITRE call the recommended solution?
Use locking functionality around code that alters or reads persistent data in a multithreaded environment.
Sources
- MITRE CWE-366: https://cwe.mitre.org/data/definitions/366.html