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

ImpactDetails
IntegrityScope: Integrity

Modify Application Data - Race conditions can corrupt shared data when multiple threads perform concurrent read-modify-write operations without synchronization.
AvailabilityScope: Availability

DoS: Crash, Exit, or Restart - Data corruption from race conditions can cause application crashes, infinite loops, or inconsistent states leading to failures.
ConfidentialityScope: 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

  1. MITRE Corporation. "CWE-567: Unsynchronized Access to Shared Data in a Multithreaded Context." https://cwe.mitre.org/data/definitions/567.html
  2. Oracle. "Java Concurrency Tutorial."
  3. CERT. "CON00-J: Synchronize access to shared mutable data."