Unsynchronized Access to Shared Data in a Multithreaded Context
Description
Unsynchronized Access to Shared Data in a Multithreaded Context is a vulnerability where a product fails to properly synchronize access to shared data resources when multiple threads can read and write the same data concurrently. Without appropriate synchronization mechanisms like locks, mutexes, or atomic operations, threads can interleave their operations in unpredictable ways, leading to race conditions. This can result in data corruption, inconsistent state, security bypasses, or application crashes when one thread reads data that another thread is simultaneously modifying.
Risk
Unsynchronized shared data access creates serious reliability and security risks. Race conditions can corrupt application state, leading to unpredictable behavior that is difficult to reproduce and debug. In security contexts, attackers may exploit race conditions to bypass authentication checks, manipulate financial transactions, or cause time-of-check-to-time-of-use (TOCTOU) vulnerabilities. Data corruption can lead to denial of service, loss of data integrity, or privilege escalation when security-critical variables are affected. These bugs are particularly dangerous because they may not manifest during testing but appear under production load conditions.
Solution
Implement proper synchronization for all shared data accessed by multiple threads. Use language-appropriate synchronization primitives such as synchronized blocks in Java, locks in Python, or mutexes in C/C++. Consider using thread-safe data structures and collections designed for concurrent access. Apply the principle of minimal sharing—reduce shared state where possible. Use atomic operations for simple counter updates. Implement immutable objects where feasible. Document thread-safety guarantees for all shared resources. Use static analysis tools to detect potential race conditions during development.
Common Consequences
| Impact | Details |
|---|---|
| Integrity | Scope: Integrity Modify Application Data - Race conditions can corrupt shared data when multiple threads perform concurrent read-modify-write operations without synchronization. |
| Availability | Scope: Availability DoS: Crash, Exit, or Restart - Data corruption from race conditions can cause application crashes, infinite loops, or inconsistent states leading to failures. |
| Confidentiality | Scope: Confidentiality Read Application Data - Threads may read partially updated data, potentially exposing sensitive information in an inconsistent state. |
Example Code
Vulnerable Code
// Vulnerable: Unsynchronized shared counter
public class VulnerableCounter {
private int count = 0; // Shared mutable state
// Vulnerable: No synchronization
public void increment() {
count++; // Not atomic: read, increment, write can be interleaved
}
public int getCount() {
return count; // May return stale value
}
}
// Vulnerable: Unsynchronized access to shared collection
public class VulnerableUserCache {
private Map<String, User> userCache = new HashMap<>(); // Not thread-safe
// Vulnerable: Concurrent modification possible
public User getUser(String userId) {
if (!userCache.containsKey(userId)) {
// Race condition: another thread may add same user
User user = loadUserFromDatabase(userId);
userCache.put(userId, user); // Concurrent put can corrupt HashMap
}
return userCache.get(userId);
}
public void updateUser(User user) {
// Vulnerable: No synchronization with getUser
userCache.put(user.getId(), user);
}
}
// Vulnerable: Check-then-act race condition
public class VulnerableAccountManager {
private Map<String, Double> balances = new HashMap<>();
// Vulnerable: TOCTOU race condition
public boolean withdraw(String accountId, double amount) {
double balance = balances.get(accountId); // Check
if (balance >= amount) { // Time gap between check and act
// Another thread could withdraw between check and update
balances.put(accountId, balance - amount); // Act
return true;
}
return false;
}
}
# Vulnerable: Unsynchronized shared state in Python
import threading
class VulnerableCounter:
def __init__(self):
self.count = 0 # Shared mutable state
# Vulnerable: No synchronization
def increment(self):
# Not atomic: read, increment, write can be interleaved
current = self.count
self.count = current + 1
def get_count(self):
return self.count
# Vulnerable: Shared singleton with race condition
class VulnerableSingleton:
_instance = None
@classmethod
def get_instance(cls):
# Vulnerable: Race condition in lazy initialization
if cls._instance is None:
# Multiple threads may pass this check simultaneously
cls._instance = cls() # Multiple instances could be created
return cls._instance
# Vulnerable: Unsynchronized bank account
class VulnerableAccount:
def __init__(self, balance):
self.balance = balance
# Vulnerable: Race condition in transfer
def transfer_to(self, target, amount):
if self.balance >= amount:
# Race: balance could change between check and update
self.balance -= amount
target.balance += amount
return True
return False
// Vulnerable: Unsynchronized shared data in C
#include <pthread.h>
#include <stdio.h>
// Vulnerable: Global shared counter without synchronization
int shared_counter = 0;
// Vulnerable: Multiple threads incrementing without lock
void* vulnerable_increment(void* arg) {
for (int i = 0; i < 100000; i++) {
// Not atomic: read-modify-write race condition
shared_counter++; // Data race!
}
return NULL;
}
// Vulnerable: Unsynchronized linked list
struct Node {
int data;
struct Node* next;
};
struct Node* head = NULL;
// Vulnerable: Concurrent list modification
void vulnerable_add_node(int data) {
struct Node* new_node = malloc(sizeof(struct Node));
new_node->data = data;
// Race condition: another thread could modify head
new_node->next = head;
head = new_node; // Lost update if another thread adds simultaneously
}
// Vulnerable: Double-checked locking (broken on many architectures)
int initialized = 0;
void* resource = NULL;
void* get_resource() {
if (!initialized) { // First check without lock
// Vulnerable: Another thread may be initializing
resource = create_resource();
initialized = 1; // May be reordered by CPU/compiler
}
return resource; // May return partially initialized resource
}
Fixed Code
// Fixed: Properly synchronized counter
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ConcurrentHashMap;
public class SecureCounter {
// Fixed: Use atomic type for thread-safe operations
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // Atomic operation
}
public int getCount() {
return count.get();
}
}
// Fixed: Thread-safe cache with proper synchronization
public class SecureUserCache {
// Fixed: Use ConcurrentHashMap for thread-safe operations
private ConcurrentHashMap<String, User> userCache = new ConcurrentHashMap<>();
public User getUser(String userId) {
// Fixed: computeIfAbsent is atomic
return userCache.computeIfAbsent(userId, id -> loadUserFromDatabase(id));
}
public void updateUser(User user) {
userCache.put(user.getId(), user);
}
}
// Fixed: Synchronized account operations
public class SecureAccountManager {
private final Map<String, Double> balances = new HashMap<>();
private final Object lock = new Object();
public boolean withdraw(String accountId, double amount) {
// Fixed: Synchronize check-then-act
synchronized (lock) {
Double balance = balances.get(accountId);
if (balance != null && balance >= amount) {
balances.put(accountId, balance - amount);
return true;
}
return false;
}
}
// Fixed: Thread-safe transfer
public boolean transfer(String fromId, String toId, double amount) {
synchronized (lock) {
Double fromBalance = balances.get(fromId);
if (fromBalance != null && fromBalance >= amount) {
balances.put(fromId, fromBalance - amount);
balances.merge(toId, amount, Double::sum);
return true;
}
return false;
}
}
}
// Fixed: Thread-safe lazy initialization
public class SecureSingleton {
// Fixed: Volatile ensures visibility across threads
private static volatile SecureSingleton instance;
private SecureSingleton() {}
public static SecureSingleton getInstance() {
// Fixed: Double-checked locking with volatile
if (instance == null) {
synchronized (SecureSingleton.class) {
if (instance == null) {
instance = new SecureSingleton();
}
}
}
return instance;
}
}
# Fixed: Properly synchronized Python code
import threading
from threading import Lock, RLock
class SecureCounter:
def __init__(self):
self._count = 0
self._lock = Lock()
def increment(self):
# Fixed: Use lock for synchronization
with self._lock:
self._count += 1
def get_count(self):
with self._lock:
return self._count
# Fixed: Thread-safe singleton with lock
class SecureSingleton:
_instance = None
_lock = Lock()
@classmethod
def get_instance(cls):
# Fixed: Double-checked locking with lock
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = cls()
return cls._instance
# Fixed: Thread-safe account with atomic operations
class SecureAccount:
def __init__(self, balance):
self._balance = balance
self._lock = RLock() # Reentrant lock for nested calls
@property
def balance(self):
with self._lock:
return self._balance
def transfer_to(self, target, amount):
# Fixed: Lock both accounts in consistent order to prevent deadlock
accounts = sorted([self, target], key=id)
with accounts[0]._lock:
with accounts[1]._lock:
if self._balance >= amount:
self._balance -= amount
target._balance += amount
return True
return False
// Fixed: Properly synchronized C code
#include <pthread.h>
#include <stdio.h>
#include <stdatomic.h>
// Fixed: Use atomic type for simple counter
atomic_int secure_counter = 0;
void* secure_increment(void* arg) {
for (int i = 0; i < 100000; i++) {
// Fixed: Atomic increment
atomic_fetch_add(&secure_counter, 1);
}
return NULL;
}
// Fixed: Synchronized linked list with mutex
struct Node {
int data;
struct Node* next;
};
struct Node* head = NULL;
pthread_mutex_t list_mutex = PTHREAD_MUTEX_INITIALIZER;
void secure_add_node(int data) {
struct Node* new_node = malloc(sizeof(struct Node));
new_node->data = data;
// Fixed: Lock before modifying shared list
pthread_mutex_lock(&list_mutex);
new_node->next = head;
head = new_node;
pthread_mutex_unlock(&list_mutex);
}
// Fixed: Thread-safe lazy initialization with pthread_once
pthread_once_t init_once = PTHREAD_ONCE_INIT;
void* resource = NULL;
void init_resource() {
resource = create_resource();
}
void* get_resource_secure() {
// Fixed: pthread_once guarantees single initialization
pthread_once(&init_once, init_resource);
return resource;
}
// Fixed: Read-write lock for read-heavy workloads
pthread_rwlock_t cache_lock = PTHREAD_RWLOCK_INITIALIZER;
struct CacheEntry* cache = NULL;
struct CacheEntry* read_cache(const char* key) {
// Fixed: Multiple readers allowed
pthread_rwlock_rdlock(&cache_lock);
struct CacheEntry* entry = find_entry(cache, key);
pthread_rwlock_unlock(&cache_lock);
return entry;
}
void write_cache(const char* key, void* value) {
// Fixed: Exclusive write access
pthread_rwlock_wrlock(&cache_lock);
update_entry(&cache, key, value);
pthread_rwlock_unlock(&cache_lock);
}
CVE Examples
- CVE-2021-21224: V8 JavaScript engine race condition in type confusion.
- CVE-2019-11815: Linux kernel race condition in net/rds/tcp.c leading to use-after-free.
- CVE-2016-9793: Linux kernel race condition in SCTP socket handling.
References
- MITRE Corporation. "CWE-567: Unsynchronized Access to Shared Data in a Multithreaded Context." https://cwe.mitre.org/data/definitions/567.html
- Oracle. "Java Concurrency Tutorial."
- CERT. "CON00-J: Synchronize access to shared mutable data."