Состояние гонки (англ. race condition), также конкуренция[1] — ошибка проектирования многопоточной системы или приложения, при которой работа системы или приложения зависит от того, в каком порядке выполняются части кода. Своё название ошибка получила от похожей ошибки проектирования электронных схем (см. Гонки сигналов).

Термин состояние гонки относится к инженерному жаргону и появился вследствие неаккуратного дословного перевода английского эквивалента. В более строгой академической среде принято использовать термин неопределённость параллелизма.

Состояние гонки — «плавающая» ошибка (гейзенбаг), проявляющаяся в случайные моменты времени и «пропадающая» при попытке её локализовать.

Возможные последствия

править

Из-за неконтролируемого доступа к общей памяти состояние гонки может приводить к совершенно различным ошибкам, которые могут проявляться в непредсказуемые моменты времени, а попытка повторения ошибки в целях отладки со схожими условиями работы может оказаться безуспешной.

Основными последствиями могут быть:

  • утечки памяти[2],
  • ошибки сегментирования[2],
  • порча данных[2],
  • уязвимости[2],
  • взаимные блокировки,
  • утечки других ресурсов, например файловых дескрипторов.

Случай с Therac-25

править

Аппарат лучевой терапии Therac-25 был первым в США медицинским аппаратом, в котором вопросы безопасности были возложены исключительно на программное обеспечение. Этот аппарат работал в трёх режимах:

  1. Электронная терапия: электронная пушка напрямую облучает пациента; компьютер задаёт энергию электронов от 5 до 25 МэВ.
  2. Рентгеновская терапия: электронная пушка облучает вольфрамовую мишень, и пациент облучается рентгеновскими лучами, проходящими через конусообразный рассеиватель. В этом режиме энергия электронов постоянна: 25 МэВ.
  3. В третьем режиме никакого излучения не было. На пути электронов (на случай аварии) располагается стальной отражатель, а излучение имитируется светом. Этот режим применяется для того, чтобы точно навести пучок на больное место.

Эти три режима задавались вращающимся диском, в котором было отверстие с отклоняющими магнитами для электронной терапии, и мишень с рассеивателем для рентгеновской. Из-за состояния гонки между управляющей программой и обработчиком клавиатуры иногда случалось, что в режиме рентгеновской терапии диск оказывался в положении «Электронная терапия», и пациент напрямую облучался пучком электронов в 25 МэВ, что вело к переоблучению. При этом датчики выводили «Нулевая доза», поэтому оператор мог повторить процедуру, усугубляя ситуацию. В результате погибли как минимум два пациента.

Часть кода была взята из Therac-6 и Therac-20. При этом в Therac-6 не было рентгеновской терапии, а в Therac-20 были аппаратные меры безопасности, которые не давали включить излучение, когда диск был в неправильном положении.

Пример

править

Рассмотрим пример кода (на Java).

volatile int x;
// Поток 1:
while (!stop) {
  x++;
  
}
// Поток 2:
while (!stop) {
  if (x%2 == 0)
    System.out.println("x=" + x);
  
}

Пусть x=0. Предположим, что выполнение программы происходит в таком порядке:

  1. Оператор if в потоке 2 проверяет x на чётность.
  2. Оператор «x++» в потоке 1 увеличивает x на единицу.
  3. Оператор вывода в потоке 2 выводит «x=1», хотя, казалось бы, переменная проверена на чётность.

Способы решения

править

Локальная копия

править

Самый простой способ решения — копирование переменной x в локальную переменную. Вот исправленный код:

// Поток 2:
while (!stop)
{
  int cached_x = x;
  if (cached_x%2 == 0)
    System.out.println("x=" + cached_x);
  
}

Естественно, этот способ работает только тогда, когда переменная одна и копирование производится за одну машинную команду.

Синхронизация

править

Более сложный и «дорогой», но и более универсальный метод решения — синхронизация потоков, а именно:

int x;
// Поток 1:
while (!stop)
{
  synchronized(someObject)
  {
    x++;
  }
  
}
// Поток 2:
while (!stop)
{
  synchronized(someObject)
  {
    if (x%2 == 0)
      System.out.println("x=" + x);
  }
  
}

Здесь семантика happens before не требует ключевое слово volatile.

Комбинированный способ

править

Предположим, что переменных — две (и ключевое слово volatile не действует), а во втором потоке вместо System.out.println стоит более сложная обработка. В этом случае оба метода неудовлетворительны: первый — потому что одна переменная может измениться, пока копируется другая; второй — потому что засинхронизирован слишком большой объём кода.

Эти способы можно скомбинировать, копируя «опасные» переменные в синхронизированном блоке. С одной стороны, это снимет ограничение на одну машинную команду, с другой — позволит избавиться от слишком больших синхроблоков.

volatile int x1, x2;
// Поток 1:
while (!stop)
{
  synchronized(someObject)
  {
    x1++;
    x2++;
  }
  
}
// Поток 2:
while (!stop)
{
  int cached_x1, cached_x2;
  synchronized (someObject)
  {
    cached_x1 = x1;
    cached_x2 = x2;
  }
  if ((cached_x1 + cached_x2)%100 == 0)
    DoSomethingComplicated(cached_x1, cached_x2);
  
}

Очевидных способов выявления и исправления состояний гонки не существует. Лучший способ избавиться от гонок — правильное проектирование многозадачной системы.

Взломы путём эксплуатирования состояния гонки

править

Существует класс ошибок (и эксплуатирующих их типов атак), позволяющих непривилегированной программе влиять на работу других программ через возможность изменения общедоступных ресурсов (обычно — вре́менных файлов; англ. /tmp race — состояние гонки во вре́менном каталоге), в определённое временно́е окно, в которое файл по ошибке программиста доступен для записи всем или части пользователей системы.

Атакующая программа может разрушить содержимое файла, вызвав аварийное завершение программы-жертвы, или, подменив данные, заставить программу выполнить какое-либо действие на уровне своих привилегий.

Именно по этой причине ПО с серьёзными требованиями по безопасности, такое, как веб-браузер, использует случайные числа криптографического качества для именования временных файлов.

См. также

править

Примечания

править
  1. Реймонд, Эрик С. Искусство программирования для Unix / пер. с англ. — М.: Издательский дом «Вильямс», 2005. — С. 202. — 544 с. — ISBN 5-8459-0791-8.
  2. 1 2 3 4 Greg Kroah-Hartman, Alessandro Rubini, Jonathan Corbet. Chapter 5. Concurrency and Race Conditions // Linux Device Drivers. — 3rd Edition. — O'Reilly Media, Inc., 2005. — ISBN 0596005903. Архивировано 12 апреля 2019 года.