Files
ubbook/concurrency/signal_unsafe.md
Dmitry Sviridkin 7ded140c5c Update signal_unsafe.md
add RegreSSHion
2024-09-08 15:51:08 +01:00

62 lines
7.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Сигнало(не)безопасность
Разработчик любого сколько-нибудь серьезного приложения рано или поздно вынужден озаботиться
вопросами поведения программы в различных краевых и внештатных ситуациях: запрос досрочного завершения, внезапное закрытие терминала, обработка маловероятных ошибочных состояний. Во многих этих случаях приходится иметь дело с довольно примитивным механизмом межпроцессного взаимодействия — с обработкой сигналов.
Программист регистрирует обработчики нужных ему сигналов и забот не знает, очень часть допуская серьезную ошибку —
выполняет в обработчике сигналов код, который там выполнять небезопасно: выделяет память, делает I/O, захватывает блокировки...
Сигналы прерывают нормальный ход исполнения программы и могут быть обработаны в произвольном потоке.
Поток мог начать выделять память, захватить блокировку в аллокаторе и в этот момент быть прерванным сигналом. Если обработчик сигнала в свою очередь запросит выделение памяти... Будет повторный захват блокировки в одном и том же потоке. Неопределенное поведение.
И результат может быть самым неожиданным. Например, в OpenSSH в 2006 году была обнаружена критическая уязвимость, с возможностью удаленно получить root доступ к системам с запущенным sshd сервером. Баг непосредственно связан с кодом, вызывавшим malloc и free при обработке сигналов. Уязвимость исправили, но в 2020, спустя 14 лет, ee случайно занесли обратно. Ошибку снова обнаружили и исправили лишь в 2024 году, и кто знает сколько раз и кто воспользовался этой [RegreSSHion](https://en.wikipedia.org/wiki/RegreSSHion) за 4 года!
Очень легко можно продемонстрировать проблему на следующем примере
```C++
std::mutex global_lock;
int main() {
std::signal(SIGINT, [](int){
std::scoped_lock lock {global_lock};
printf("SIGINT!\n");
});
{
std::scoped_lock lock {global_lock};
printf("start long job\n");
sleep(10);
printf("end long job\n");
}
sleep(10);
}
```
Если мы скомпилируем эту программу под Linux (не забыв указать `-pthread`), запустим и нажмем `Ctrl+C`, то она зависнет навсегда из-за повторного захвата мьютекса одним и тем же потоком. Если же забудем `-pthread`, то не зависнет и отработает «ожидаемым» образом.
Под Windows эта программа также работает «ожидаемо» из-за специфики обработки сигналов — там для обработки `SIGINT`/`SIGTERM` всегда неявно порождается новый поток.
В любом случае этот код некорректен из-за использования сигналонебезопасной функции внутри обработчика сигналов.
Обработка сигналов — вопрос крайне платформоспецифичный и зависит от конкретной прикладной задачи и архитектуры вашего приложения. Также это довольно сложный вопрос, если учитывать, что во время обработки одного сигнала нас могут прервать для обработки другого.
Наиболее часто встречаемое использование обработки сигналов — корректное завершение приложения, с очисткой ресурсов, закрытием соединений — graceful shutdown. В таком случае обычно обработка сигналов сводится к выставлению и проверке некоторого глобального флага.
Стандарты C и C++ описывают специальный целочисленный тип — `sig_atomic_t`. При доступе к переменным этого типа гарантируется сигналобезопасность. На практике этот тип может оказаться просто алиасом для `int` или `long`. `volatile sig_atomic_t` можно использовать в качестве глобального флага, выставляемого в обработчике сигналов. Но только в однопоточной среде. Тут `volatile` необходим только для предотвращения нежелательной оптимизаций — компилятор не делает предположений о возможной обработке сигналов и прерывании нормального потока выполнения программы.
Нужно помнить, что `volatile` не дает гарантий потокобезопасности. И в многопоточной среде необходимо использовать настоящие атомарные типы, поддерживаемые на вашей платформе. Например, `std::atomic<int>`. Если, конечно, `std::atomic<T>::is_lock_free` истинно.
### Как бороться?
1. Делать обработчики сигналов как можно более простыми
2. Отключать автоматический прием сигналов и выполнять их обработку в рамках обычного исполнения программы (см., например, `sigprocmask` и `sigwait`)
3. Сверяться с документацией, безопасно ли использование той или иной функции в контексте обработчика сигналов
4. Для флагов обработки сигналов использовать атомарные переменные, lock-free структуры или, если приложение однопоточное, `volatile sig_atomic_t`.
### Полезные ссылки
1. https://man7.org/linux/man-pages/man7/signal-safety.7.html
2. https://www.gnu.org/software/libc/manual/html_node/Blocking-Signals.html
3. https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/signal
4. https://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_chapter/libc_24.html
5. https://en.cppreference.com/w/cpp/utility/program/sig_atomic_t