# Сигнало(не)безопасность Разработчик любого сколько-нибудь серьезного приложения рано или поздно вынужден озаботиться вопросами поведения программы в различных краевых и внештатных ситуациях: запрос досрочного завершения, внезапное закрытие терминала, обработка маловероятных ошибочных состояний. Во многих этих случаях приходится иметь дело с довольно примитивным механизмом межпроцессного взаимодействия — с обработкой сигналов. Программист регистрирует обработчики нужных ему сигналов и забот не знает, очень часть допуская серьезную ошибку — выполняет в обработчике сигналов код, который там выполнять небезопасно: выделяет память, делает 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`. Если, конечно, `std::atomic::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