Signal handler race condition¶
Bir signal handler, non-reentrant code'u kesintiye uğratır (veya onunla state paylaşır), dolayısıyla async delivery global/heap yapılarını corrupt eder — CWE-364.
Mechanism¶
Invariant
Bir signal handler, başka bir fonksiyonun ortası dahil herhangi bir instruction boundary'sinde asenkron olarak ateşlenebilir. Eğer handler, kesintiye uğrayan code'un da değiştirmekte olduğu state'e dokunursa — global/static değişkenler ya da internal allocator metadata'sı — sonuç, onu serialize edecek bir lock'u olmayan bir data race'tir. Klasik durum: bir handler'ın async-signal-safe olmayan bir fonksiyon çağırması.
Async-signal-safe küme (bkz. man 7 signal-safety) küçüktür ve malloc(), free(), printf()/syslog() ile libc'nin çoğunu dışlar, çünkü bunlar internal state'i (örneğin heap free-list'i, stdio buffer'ları) hiçbir signal-safe lock altında olmadan tutar. Bir signal geldiğinde main malloc() içindeyse ve handler malloc()/free() çağırırsa (genellikle syslog() yoluyla dolaylı olarak), ikisi de aynı yarı-güncellenmiş heap metadata'sına karşı çalışır — double-free, heap corruption ya da zaten free edilmiş bir global'in re-entrant free'i üretir. CWE-364'e göre bu, data corruption'a, code execution'a ya da — kesintiye uğrayan code ayrıcalık taşıyorsa — privilege escalation'a tırmanabilir. İki signal için kayıtlı aynı handler (örneğin SIGHUP ve SIGTERM), handler'ı fiilen kendi içine re-enter ettirir.
Walkthrough¶
Bir global'i free eden ve log'layan, SIGHUP ve SIGTERM tarafından paylaşılan bir handler — CWE-364'ün gösterici güvenlik açığı:
#include <signal.h>
#include <stdlib.h>
#include <syslog.h>
#include <string.h>
char *global2;
void *global1;
void sh(int sig) {
syslog(LOG_NOTICE, "%s\n", global2); /* syslog() -> malloc(): NOT async-signal-safe */
free(global1); /* re-entrant free of a global pointer */
global1 = NULL;
}
int main(void) {
global1 = malloc(8);
global2 = strdup("msg");
signal(SIGHUP, sh);
signal(SIGTERM, sh); /* same handler on two signals -> reentrancy */
while (1) pause();
}
İlk handler hâlâ syslog/malloc içindeyken ikinci bir signal göndererek race'i tetikle:
$ gcc -g vuln.c -o vuln && ./vuln &
[1] 5123
$ kill -HUP 5123 ; kill -TERM 5123 # SIGTERM interrupts sh() before global1 is NULLed
Beklenen çıktı — glibc, global1'in double free()'sini tespit eder:
*** Error in `./vuln': double free or corruption (fasttop): 0x000055e2a1c1b260 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7f...]
======= Memory map: ========
Aborted (core dumped)
Güvenli pattern: handler sadece bir flag set eder
Tüm gerçek işi main loop'a erteleyerek async-signal-safety korunur:
#include <signal.h>
volatile sig_atomic_t got_signal = 0; /* the only signal-safe shared type */
void handler(int sig) { got_signal = 1; } /* set-flag only; no malloc/stdio */
int main(void) {
signal(SIGTERM, handler);
for (;;) {
if (got_signal) { /* do free()/logging here, in normal context */ }
pause();
}
}
write(2) async-signal-safe'tir ve handler içinde kullanılabilir; printf/syslog/malloc/free kullanılamaz.
Detection¶
- Statik analiz: kayıtlı bir handler'dan ulaşılabilen async-signal-safe olmayan bir fonksiyona (
malloc,free,printf,syslog,setjmp/longjmp) yapılan herhangi bir çağrıyı işaretle (CERT C SIG30-C/SIG31-C). - Runtime: glibc'nin
double free or corruptionabort'ları ya da signal stress altında ASan/Valgrind'in heap tutarsızlıklarını raporlaması güçlü göstergelerdir.
Mitigation¶
- Sadece bir flag set et. Handler'lar tek bir
volatile sig_atomic_tyazıp return etmeli; gerçek işi main loop'ta yap. - Handler'lar içinde yalnızca async-signal-safe fonksiyonlar kullan (
man 7 signal-safety). - Shared state'e dokunan critical section'ların etrafında
sigprocmask()/pthread_sigmask()ile signal'leri block et; signal'leri senkron ele almak içinsignalfd()/self-pipe'ı tercih et. - Shared state'i mutate ettiğinde bir handler'ı birden çok signal için kaydetmekten kaçın.