Signal-Handler mit nicht asynchron-sicherer Funktionalität
Beschreibung
Signal-Handler mit nicht asynchron-sicherer Funktionalität ist eine Nebenläufigkeits-Schwachstelle, bei der ein Signal-Handler Codesequenzen enthält, die nicht sicher in einem asynchronen Kontext ausgeführt werden können. Signal-Handler können zu jedem Zeitpunkt während der Programmausführung aufgerufen werden und jeden gerade ausgeführten Code unterbrechen. Wenn der Handler nicht-reentrant Funktionen verwendet (wie malloc(), printf() oder syslog()), nicht-atomar auf globale oder statische Variablen zugreift oder von anderen Signalen unterbrochen werden kann, kann der Programmzustand beschädigt werden. Diese Beschädigung kann zu Abstürzen, undefiniertem Verhalten oder ausnutzbaren Sicherheitsschwachstellen führen.
Risiko
Diese Schwachstelle kann zu Speicherbeschädigung, Denial of Service oder Codeausführung führen. Wenn ein Signal-Handler nicht-reentrant Funktionen wie malloc() aufruft, könnte der Handler dieselbe Funktion unterbrechen, die bereits im Hauptcode ausgeführt wird, was Heap-Metadaten beschädigt und zu Abstürzen oder Ausnutzung führt. Globale Variablen, auf die in Handlern zugegriffen wird, können in inkonsistenten Zuständen hinterlassen werden, wenn Handler unterbrochen werden. Angreifer, die Signale an einen Prozess senden können, können ihre Angriffe timen, um diese Race Conditions auszulösen, was möglicherweise zu Privilegieneskalation oder Sicherheitsumgehung führt. Die Schwachstelle ist besonders gefährlich, weil dieselbe Handler-Funktion beide Signal-Aufrufe verarbeitet.
Lösung
Verwenden Sie in Signal-Handlern nur asynchron-signal-sichere Funktionen. POSIX definiert eine spezifische Liste von Funktionen, die sicher aus Signal-Handlern aufgerufen werden können - beschränken Sie Handler auf diese Liste. Allokieren oder geben Sie keinen Speicher in Handlern frei. Verwenden Sie volatile sig_atomic_t für alle Variablen, die zwischen Handlern und Hauptcode geteilt werden. Blockieren Sie Signale während kritischer Abschnitte, die auf geteilten Zustand zugreifen. Entwerfen Sie Handler so, dass sie nur ein Flag setzen, das die Hauptprogrammschleife prüft, anstatt komplexe Operationen direkt auszuführen. Wenn komplexe Verarbeitung erforderlich ist, lassen Sie den Handler in eine Pipe schreiben, die das Hauptprogramm liest. Erwägen Sie die Verwendung von signalfd() oder ähnlichen Mechanismen, die Signale in Event-Loops integrieren.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Verfügbarkeit | Bereich: Verfügbarkeit DoS: Absturz, Beendigung oder Neustart - Zustandsbeschädigung durch nicht-reentrant Funktionsaufrufe oder Race Conditions verursacht Abstürze oder Hänger. |
| Integrität, Vertraulichkeit, Verfügbarkeit | Bereich: Integrität, Vertraulichkeit, Verfügbarkeit Unerlaubten Code oder Befehle ausführen - Wenn Handler sicherheitskritischen Zustand beschädigen, können Angreifer möglicherweise Schutzmassnahmen umgehen oder Codeausführung erreichen. |
Beispielcode
Anfälliger Code
// Anfällig: Handler ruft nicht-reentrant Funktionen auf
#include <signal.h>
#include <syslog.h>
#include <stdlib.h>
char *logMessage;
void handler(int sigNum) {
// Anfällig: syslog() ist nicht asynchron-signal-sicher
// Es ruft intern malloc() auf, das nicht reentrant ist
syslog(LOG_NOTICE, "%s\n", logMessage);
// Anfällig: free() ist nicht asynchron-signal-sicher
free(logMessage);
exit(0);
}
int main() {
logMessage = strdup("Wird beendet");
signal(SIGHUP, handler);
signal(SIGTERM, handler);
// Hauptverarbeitungsschleife
while (1) {
// Wenn SIGHUP während malloc() im Hauptcode ankommt,
// beschädigt syslog()'s malloc() den Heap
}
}
// Anfällig: Double-free via Signal-Reentranz
#include <signal.h>
#include <stdlib.h>
char *global1;
char *global2;
void sh(int dummy) {
// Anfällig: Kann zweimal aufgerufen werden wenn zwei Signale ankommen
syslog(LOG_NOTICE, "%s\n", global1);
free(global2);
free(global1); // Wenn Handler reentrant ist, double-free!
exit(0);
}
int main() {
global1 = strdup("nachricht");
global2 = strdup("daten");
signal(SIGHUP, sh);
signal(SIGTERM, sh);
// SIGHUP kommt an, beginnt global1 freizugeben
// SIGTERM kommt vor exit() an, ruft sh() erneut auf
// global1 zweimal freigegeben!
while (1) { /* arbeiten */ }
}
Korrigierter Code
// Korrigiert: Nur asynchron-signal-sichere Funktionen verwenden
#include <signal.h>
#include <unistd.h>
#include <string.h>
volatile sig_atomic_t shutdown_requested = 0;
void handler(int sigNum) {
// Korrigiert: Nur ein Flag setzen
shutdown_requested = 1;
// Wenn Sie ausgeben müssen, verwenden Sie write(), das asynchron-signal-sicher ist
const char *msg = "Shutdown-Signal empfangen\n";
write(STDERR_FILENO, msg, strlen(msg));
}
int main() {
char *logMessage = strdup("Wird beendet");
signal(SIGHUP, handler);
signal(SIGTERM, handler);
while (!shutdown_requested) {
// Hauptverarbeitung
}
// Aufräumen im Hauptkontext wo es sicher ist
syslog(LOG_NOTICE, "%s\n", logMessage);
free(logMessage);
return 0;
}
// Korrigiert: Signale während kritischer Abschnitte blockieren
#include <signal.h>
#include <stdlib.h>
char *global1;
char *global2;
volatile sig_atomic_t should_exit = 0;
void sh(int dummy) {
// Korrigiert: Nur Flag setzen
should_exit = 1;
}
int main() {
sigset_t mask, oldmask;
global1 = strdup("nachricht");
global2 = strdup("daten");
signal(SIGHUP, sh);
signal(SIGTERM, sh);
while (!should_exit) {
// Arbeiten
}
// Korrigiert: Signale während Aufräumen blockieren
sigemptyset(&mask);
sigaddset(&mask, SIGHUP);
sigaddset(&mask, SIGTERM);
sigprocmask(SIG_BLOCK, &mask, &oldmask);
// Sicheres Aufräumen - Signale blockiert
syslog(LOG_NOTICE, "%s\n", global1);
free(global2);
free(global1);
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return 0;
}
// Korrigiert: Self-Pipe-Trick für komplexe Handler-Logik verwenden
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
int signal_pipe[2];
void handler(int sig) {
// Korrigiert: write() ist asynchron-signal-sicher
char c = (char)sig;
write(signal_pipe[1], &c, 1);
}
int main() {
pipe(signal_pipe);
// Schreibende nicht-blockierend machen
fcntl(signal_pipe[1], F_SETFL, O_NONBLOCK);
signal(SIGHUP, handler);
signal(SIGTERM, handler);
while (1) {
fd_set fds;
FD_ZERO(&fds);
FD_SET(signal_pipe[0], &fds);
FD_SET(STDIN_FILENO, &fds);
// select() auf sowohl regulärer I/O als auch Signal-Pipe
if (select(signal_pipe[0] + 1, &fds, NULL, NULL, NULL) > 0) {
if (FD_ISSET(signal_pipe[0], &fds)) {
char sig;
read(signal_pipe[0], &sig, 1);
// Jetzt sicher komplexe Verarbeitung im Hauptkontext durchzuführen
printf("Signal %d empfangen\n", (int)sig);
// Kann sicher syslog, free, etc. hier aufrufen
}
}
}
}
Verwandte CWEs
- CWE-364: Signal-Handler Race Condition (Eltern)
- CWE-479: Signal-Handler-Verwendung einer nicht-reentrant Funktion (Kind)
- CWE-831: Signal-Handler-Funktion mit mehreren Signalen assoziiert (verwandt)
- CWE-662: Unzureichende Synchronisation (verwandt)
Referenzen
- MITRE Corporation. "CWE-828: Signal Handler with Functionality that is not Asynchronous-Safe." https://cwe.mitre.org/data/definitions/828.html
- CERT C Secure Coding Standard. "SIG30-C. Call only asynchronous-safe functions within signal handlers."
- POSIX. "Signal Concepts - Async-Signal-Safe Functions."