mirror of
https://github.com/Nekrolm/ubbook.git
synced 2025-12-18 05:14:34 +03:00
62 lines
7.6 KiB
Markdown
62 lines
7.6 KiB
Markdown
# Сигнало(не)безопасность
|
||
|
||
Разработчик любого сколько-нибудь серьезного приложения рано или поздно вынужден озаботиться
|
||
вопросами поведения программы в различных краевых и внештатных ситуациях: запрос досрочного завершения, внезапное закрытие терминала, обработка маловероятных ошибочных состояний. Во многих этих случаях приходится иметь дело с довольно примитивным механизмом межпроцессного взаимодействия — с обработкой сигналов.
|
||
|
||
Программист регистрирует обработчики нужных ему сигналов и забот не знает, очень часть допуская серьезную ошибку —
|
||
выполняет в обработчике сигналов код, который там выполнять небезопасно: выделяет память, делает 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
|