Skip to content

Latest commit

 

History

History
61 lines (43 loc) · 7.65 KB

signal_unsafe.md

File metadata and controls

61 lines (43 loc) · 7.65 KB

Сигнало(не)безопасность

Разработчик любого сколько-нибудь серьезного приложения рано или поздно вынужден озаботиться вопросами поведения программы в различных краевых и внештатных ситуациях: запрос досрочного завершения, внезапное закрытие терминала, обработка маловероятных ошибочных состояний. Во многих этих случаях приходится иметь дело с довольно примитивным механизмом межпроцессного взаимодействия — с обработкой сигналов.

Программист регистрирует обработчики нужных ему сигналов и забот не знает, очень часть допуская серьезную ошибку — выполняет в обработчике сигналов код, который там выполнять небезопасно: выделяет память, делает I/O, захватывает блокировки...

Сигналы прерывают нормальный ход исполнения программы и могут быть обработаны в произвольном потоке. Поток мог начать выделять память, захватить блокировку в аллокаторе и в этот момент быть прерванным сигналом. Если обработчик сигнала в свою очередь запросит выделение памяти... Будет повторный захват блокировки в одном и том же потоке. Неопределенное поведение.

И результат может быть самым неожиданным. Например, в OpenSSH в 2006 году была обнаружена критическая уязвимость, с возможностью удаленно получить root доступ к системам с запущенным sshd сервером. Баг непосредственно связан с кодом, вызывавшим malloc и free при обработке сигналов. Уязвимость исправили, но в 2020, спустя 14 лет, ee случайно занесли обратно. Ошибку снова обнаружили и исправили лишь в 2024 году, и кто знает сколько раз и кто воспользовался этой RegreSSHion за 4 года!

Очень легко можно продемонстрировать проблему на следующем примере

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