Состояние гонки

Состояние гонки (англ. 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 года.