Race Condition: когда миллисекунды решают всё
Привет, друг!
Race Condition (состояние гонки) — это когда два или больше потоков/процессов пытаются одновременно работать с общим ресурсом, и результат зависит от того, кто первый добежал. Звучит безобидно, но на практике это адская дыра в безопасности и стабильности систем.
Почему это критично
Представь: у тебя есть счётчик посетителей сайта. Два запроса приходят одновременно, оба читают значение 100, оба добавляют +1 и записывают 101. А должно быть 102. Теперь умножь это на банковские транзакции или систему бронирования билетов — вот где начинается веселье.
Классический пример на Python
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import threading counter = 0 def increment(): global counter for _ in range(100000): temp = counter # Читаем temp += 1 # Увеличиваем counter = temp # Записываем threads = [threading.Thread(target=increment) for _ in range(10)] for t in threads: t.start() for t in threads: t.join() print(f"Ожидали: 1000000, получили: {counter}") # Результат будет меньше миллиона |
Видишь проблему? Между чтением и записью есть окно уязвимости. Если другой поток успеет изменить counter в это время — твои данные полетели.
Эксплойт реального мира: TOCTOU
Time-Of-Check to Time-Of-Use — классика жанра. Проверяешь условие, потом используешь ресурс. Между этими моментами что-то меняется.
|
1 2 3 4 5 6 |
import os # Уязвимый код if not os.path.exists('/tmp/safe_file'): with open('/tmp/safe_file', 'w') as f: f.write('safe data') |
Атакующий между проверкой и созданием может подсунуть симлинк на /etc/passwd. Поздравляю, ты только что перезаписал системный файл.
Фикс: атомарные операции и блокировки
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import threading counter = 0 lock = threading.Lock() def increment_safe(): global counter for _ in range(100000): with lock: # Захватываем блокировку counter += 1 threads = [threading.Thread(target=increment_safe) for _ in range(10)] for t in threads: t.start() for t in threads: t.join() print(f"Теперь точно: {counter}") # Всегда будет 1000000 |
Блокировка гарантирует, что только один поток работает с переменной в момент времени.
Продвинутые техники
Атомарные операции через threading.atomic:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
from threading import Lock import time class BankAccount: def __init__(self, balance): self.balance = balance self.lock = Lock() def withdraw(self, amount): with self.lock: if self.balance >= amount: time.sleep(0.001) # Имитация задержки self.balance -= amount return True return False # Без lock два потока могли бы снять больше, чем есть |
Использование семафоров для ограничения доступа:
|
1 2 3 4 5 6 7 8 9 |
from threading import Semaphore max_connections = 5 semaphore = Semaphore(max_connections) def connect_to_resource(): with semaphore: # Максимум 5 потоков одновременно print("Работаем с ресурсом") |
Race Condition в веб-приложениях
Классический сценарий: проверка баланса перед покупкой.
|
1 2 3 4 5 6 7 8 9 |
# Уязвимый код (Flask) @app.route('/buy', methods=['POST']) def buy_item(): user_balance = get_balance(user_id) # 1. Читаем if user_balance >= item_price: # 2. Проверяем # ТУТ может прийти второй запрос update_balance(user_id, -item_price) # 3. Списываем return "Куплено" return "Недостаточно средств" |
Если юзер отправит два запроса одновременно, оба пройдут проверку до списания. Результат: купил два раза за цену одного.
Правильное решение:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
from sqlalchemy import update @app.route('/buy', methods=['POST']) def buy_item_safe(): # Атомарное обновление с проверкой result = db.session.execute( update(User) .where(User.id == user_id) .where(User.balance >= item_price) .values(balance=User.balance - item_price) ) if result.rowcount > 0: db.session.commit() return "Куплено" return "Недостаточно средств" |
Тут проверка и списание происходят атомарно на уровне базы данных.
Как ловить Race Conditions
Стресс-тестирование:
|
1 2 3 4 5 6 7 8 9 |
import concurrent.futures def stress_test(): with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor: futures = [executor.submit(vulnerable_function) for _ in range(1000)] results = [f.result() for f in futures] # Анализируй результаты на несоответствия print(f"Уникальных результатов: {len(set(results))}") |
Race Detector в Go:
|
1 |
go run -race main.go |
Python такого из коробки не имеет, но можно использовать ThreadSanitizer через C-расширения.
Защита файловой системы
|
1 2 3 4 5 6 7 8 9 10 11 |
import os import fcntl def safe_file_write(filename, data): fd = os.open(filename, os.O_CREAT | os.O_EXCL | os.O_WRONLY) try: fcntl.flock(fd, fcntl.LOCK_EX) # Эксклюзивная блокировка os.write(fd, data.encode()) finally: fcntl.flock(fd, fcntl.LOCK_UN) os.close(fd) |
O_EXCL + O_CREAT гарантируют, что файл создастся атомарно или упадёт с ошибкой, если уже существует.
Чеклист для защиты
- Используй блокировки для критических секций
- Атомарные операции на уровне БД (SELECT FOR UPDATE, ACID-транзакции)
- Избегай TOCTOU — проверка и использование должны быть атомарными
- Стресс-тестируй многопоточный код
- Не полагайся на «это маловероятно» — в продакшене всё случается
- Используй deadlock detection и timeout для блокировок
Реальный эксплойт: двойная трата
В 2010 году в Bitcoin была найдена уязвимость, где можно было потратить одни и те же монеты дважды из-за race condition в обработке транзакций. Патч вышел за часы, но это показало, что даже в критичных системах такие баги возможны.
Лайфхак: дебаг race conditions
Добавь случайные задержки в критические места:
|
1 2 3 4 5 6 7 8 9 |
import random import time def increment_debug(): global counter temp = counter time.sleep(random.uniform(0.0001, 0.001)) # Увеличиваем вероятность race temp += 1 counter = temp |
Если с задержками всё падает — у тебя race condition. Теперь чини через блокировки.
Итого
Race Condition — это не теория, а реальная угроза. Каждая миллисекунда между чтением и записью — окно для эксплойта. Блокировки, атомарные операции и правильная архитектура — твоя броня. Никогда не полагайся на «быстродействие» как на защиту. В многопоточном мире скорость — твой враг, а синхронизация — твой друг.

На этом все. Всем хорошего дня!
