Yazılım testlerinden geçmiş, on yıldır canlıda çalışan bir kod satırı insanı öldürebilir mi?
Therac-25 vakası bunu mümkün kılan koşulların bir kataloğudur: yeniden kullanılan ama doğrulanmayan kod, kaldırılan donanım güvenlik kilitleri, anlaşılmaz hata mesajları ve hepsinden önemlisi — hatayı bulmayı neredeyse imkânsız kılan bir zamanlama bağımlılığı.
1985 ile 1987 yılları arasında Kuzey Amerika'daki birkaç kanser kliniğinde tuhaf şeyler olmaya başladı. Radyasyon tedavisi gören hastalar tedavinin hemen ardından yanma, ağrı ve şişme bildiriyordu. Bazıları haftalar içinde öldü. Bazıları hayatta kaldı ama kalıcı hasarla.
Hepsinin ortak noktası aynı makineyi kullanmış olmalarıydı: Therac-25.
Therac-25, Kanadalı AECL (Atomic Energy of Canada Limited) şirketinin ürettiği bir radyasyon tedavi cihazıydı. İki farklı tedavi modunda çalışabiliyordu:
Doğru çalışırken makine, operatörün seçtiği moda göre mekanik bileşenleri doğru konuma otomatik olarak getiriyor, ardından demeti hastaya yönlendiriyordu.
Therac-25'in önceki modeli Therac-20, donanım seviyesinde güvenlik kilitlerine sahipti: yanlış mod seçildiğinde fiziksel bir mekanizma demeti engellerdi. Therac-25, bu donanım kilitlerini kaldırdı. Güvenlik tamamen yazılıma bırakıldı.
Bu karar ölümcül oldu.
Therac-25 kazalarının arkasında birbirini besleyen iki yazılım hatası yatıyordu.
Makineyi uzun süre kullanan operatörler hızlı yazma alışkanlığı kazanmıştı. Sistem, operatörün tedavi parametrelerini girdikten sonra bir onay ekranı gösteriyordu. Burada "yukarı ok" tuşuna basarak önceki alana dönüp düzeltme yapılabiliyordu.
Sorun şuydu: 8 saniyeden daha kısa sürede "yukarı ok → düzelt → onayla" işlemi yapılırsa, arka planda çalışan kontrol döngüsü (control loop) henüz mod değişikliğini işlememişti.
Yazılım şöyle çalışıyordu:
/* Basitleştirilmiş — gerçek kod Assembly'deydi */
void setup_treatment(void) {
if (mode == XRAY) {
position_flattening_filter(); /* Metali yerleştir */
set_high_energy_beam();
} else {
retract_flattening_filter(); /* Metali çek */
set_low_energy_beam();
}
}
/* Ana döngü — mod değişikliği burada işleniyor */
void main_loop(void) {
while (1) {
read_operator_input(); /* Bu fonksiyon 'mode' değişkenini günceller */
setup_treatment(); /* Bu fonksiyon yeni 'mode' değerini okur */
/* ... */
}
}
Operatör hızlı davranırsa read_operator_input() yeni modu yazarken setup_treatment() henüz eski modla çalışıyordu. Sonuç: metal hedef yerinde değilken yüksek enerjili X-ışını demeti hastaya gönderildi.
Bu bir yarış koşuluydu: iki süreç aynı bellek konumunu (mode değişkeni) eşzamansız olarak okuyup yazıyordu ve bu erişimler senkronize değildi.
İkinci hata daha sinsi bir biçimde çalışıyordu.
Yazılımda, tedavi kurulum döngüsünün kaç kez tamamlandığını sayan bir değişken vardı. Bu değişken 8-bit işaretsiz tamsayıydı (uint8_t) — 0'dan 255'e kadar sayabiliyordu.
uint8_t setup_count = 0;
void increment_setup(void) {
setup_count++; /* 255 + 1 = 0 → "hata yok" kodu */
}
bool is_ready(void) {
return (setup_count == 0); /* Hata değil, hazır! */
}
Her 256 tedavide bir, güvenlik kontrolü yanlış olarak "hazır" durumuna geçiyordu. Yarış koşuluyla bu taşma bir araya geldiğinde, sistem hiçbir hata vermeden hastaya normalin 25.000 katı radyasyon dozu gönderdi.
Kazalar 1985'te başladı; AECL hatayı ancak 1987'de tespit edebildi. Bu gecikmenin birkaç nedeni var:
Semptomlar başka nedenlere bağlandı. Hastaların şikayetleri başlangıçta yanlış kurulum, hasta hareketi veya başka tıbbi nedenlerle açıklandı. Radyasyon zehirlenmesi hemen belli olmaz.
Hata üretmek için belirli bir zamanlaması gerekiyordu. Yarış koşulu yalnızca deneyimli operatörler hızlı yazım yaptığında tetikleniyordu. Yeni başlayan operatörler daha yavaş yazdığından, makineleri hiç sorun çıkarmadı. Bu durum sorunu izole etmeyi son derece güçleştirdi.
Test prosedürleri yetersizdi. AECL, Therac-25 yazılımını Therac-20'den büyük ölçüde yeniden kullandı. Güvenlik testleri donanım kilitleri kaldırıldıktan sonra güncellenmedi.
Hata mesajları anlamsızdı. Makine bir hata oluştuğunda ekranda MALFUNCTION 54 gibi bir kod gösteriyordu. Bu kodun ne anlama geldiği ne el kitabında ne de servis belgelerinde açıklanmıştı.
Kazalarda 6 kişi hayatını kaybetti, en az 3 kişi kalıcı sakatlandı. FDA, 1988'de AECL'e kapsamlı bir uyarı mektubu gönderdi. Makine piyasadan çekildi.
Bu dava, yazılım mühendisliği tarihinin en çok atıfta bulunulan güvenlik olaylarından biri haline geldi. Sonrasında:
Birden fazla görev (veya kesme, interrupt) aynı değişkene erişiyorsa, bu erişimi koruyun:
/* Kötü: kesme ve ana döngü aynı değişkeni korumasız okuyup yazıyor */
volatile uint8_t mode;
void operator_input_irq(void) {
mode = get_new_mode(); /* Kesme: yazar */
}
void setup_treatment(void) {
if (mode == XRAY) { ... } /* Ana döngü: okur — race condition! */
}
/* İyi: kritik bölümü atomik bir işlem içinde koru */
void setup_treatment(void) {
__disable_irq(); /* Kesmeleri geçici kapat */
uint8_t current_mode = mode; /* Atomik okuma */
__enable_irq();
if (current_mode == XRAY) { ... }
}
/* Kötü: 8-bit taşması güvenlik koşulunu sıfırlıyor */
uint8_t setup_count = 0;
setup_count++;
/* İyi: taşmayı açıkça doyur ya da 32-bit kullan */
if (setup_count < UINT8_MAX) {
setup_count++;
} else {
enter_fault_state();
}
Güvenlik açısından kritik sistemlerde yazılım hatalarına karşı bir donanım güvencesi bulunmalıdır. Bir röle, bir fiziksel kilit, bir watchdog timer — yazılım başarısız olduğunda bile sistemi güvenli konuma getirecek bağımsız bir mekanizma.
/* Kötü */
error_code = 54;
/* İyi */
typedef enum {
ERR_BEAM_INTERLOCK_FAILED = 1,
ERR_FILTER_POSITION_MISMATCH,
ERR_SETUP_COUNTER_OVERFLOW,
} SystemError;
void report_error(SystemError err) {
log_error(err);
display_human_readable_message(err);
halt_beam_immediately();
}
Bir güvenlik kontrolü, yalnızca "normal koşulda çalışıyor mu?" diye test edilmemeli; "güvenlik koşulu tetiklendiğinde doğru davranıyor mu?" diye de test edilmelidir. Her güvenlik yolu bağımsız olarak doğrulanmalıdır.
| Hata | Açıklama |
|---|---|
| Yarış koşulu | Operatör hızı, mod güncellemesi ile doz hesabı arasında senkronizasyon bozdu |
| Tamsayı taşması | uint8_t sayaç 255'ten 0'a döndü — "hata yok" kodunu tetikledi |
| Donanım kilidi kaldırıldı | Güvenlik tamamen yazılıma bırakıldı; yazılım başarısız olunca koruma kalmadı |
| Yetersiz test | Güvenlik yolları bağımsız olarak test edilmedi |
| Anlaşılmaz hatalar | MALFUNCTION 54 — ne anlama geldiğini kimse bilmiyordu |
| Sonuç | 6 ölü, 3 kalıcı sakat, sektör çapında yeni güvenlik standartları |