Therac-25 and Beyond: Engineering Lessons from a Software-Induced Tragická Nehoda
When we hear the phrase tragická nehoda-Czech for "tragic accident"-the mind often jumps to traffic collisions, industrial disasters. Or natural catastrophes. But for senior software engineers, the most haunting tragedies are the ones we caused ourselves. Between 1985 and 1987, six cancer patients died from massive radiation overdoses delivered by the Therac-25, a radiation therapy machine controlled by a software system that failed catastrophically. This was not a mechanical failure. It was a software failure-a race condition in the program that allowed the electron beam to fire at full power without the rotating target in place. The result: tragická nehoda that changed how the industry thinks about safety-critical code.
Today, half a century later, we still see echoes of the same mistakes in avionics, automotive controls. And medical devices. The Boeing 737 MAX MCAS software, the Stuxnet worm's logic flaws. And the 2017 Amazon S3 outage that broke the internet all share root causes that can be traced to the same engineering blind spots. As a senior engineer who has worked on safety systems for embedded devices for over a decade, I can tell you that the gap between "it passes unit tests" and "it kills nobody" is wider than most teams realise. In this article, I'll dissect the anatomy of a software-induced tragedy, provide concrete methodologies to prevent it. And argue that engineering culture matters more than any static analysis tool.
One of the most tragic accidents in software history resulted from a race condition in a radiation therapy machine and its lessons remain urgent for every engineer building safety-critical systems today,
Therac-25: A Case Study in Software-Induced Tragic Accident
The Therac-25 was a linear accelerator designed by Atomic Energy of Canada Limited (AECL). It could deliver two modes: a low‑power electron beam (5-25 MeV) for shallow tumours. And a high‑power X‑ray mode that required a rotating tungsten target and an electron beam at much higher energy (25 MeV). The machine's predecessor, the Therac-20, had independent hardware interlocking; the Therac-25 relied on software alone for safety checks. That decision proved fatal.
The infamous bug was a race condition in the display‑update and parameter‑check tasks. When operators edited the treatment field size in a particular sequence, the software could skip the sanity check on magnet current and allow the electron beam to fire at full power for the X‑ray mode without the target in place. The beam hit patients directly, delivering up to 25 000 rads - enough to cause acute radiation poisoning and death within days. Investigations by the FDA and independent researchers (Leveson & Turner, 1993) revealed that AECL had reused code from the Therac-20 without updating its safety analysis. And that the software was never formally tested for concurrent failures.
This tragická nehoda isn't a distant cautionary tale; it's a canonical example of three mistakes that still recur: (1) trusting software as a primary safety mechanism without hardware redundancy, (2) ignoring race conditions during concurrency analysis, and (3) shipping code that had never been run through worst‑case scenario integration tests. The Therac-25 killed people because engineers believed a single‑process state machine could guarantee patient safety. We now know better - or do we?
Root Causes: Race Conditions, Lack of Redundancy, and Poor Testing
Every software engineer has encountered a race condition. Typically it causes a flickering UI or a corrupted log file. In safety‑critical systems, it kills. The Therac-25's race condition wasn't subtle: two concurrent tasks (a keyboard handler and a dose‑setup routine) both accessed the same flag without mutual exclusion. The fix, after the accidents, was to add a mutex - but that alone was insufficient because the system also lacked a second line of defence.
Modern safety standards (IEC 62304 for medical devices, DO-178C for avionics) mandate two independent means of detecting a failure. In the Therac-25, the only check that prevented overdose was the software's own comparison of the digital‑to‑analog converter output to the set point. No hardware watchdog, no redundant sensor, no cross‑channel comparison existed. This is a fundamental architectural flaw: if the software miscomputes the set point, it will also miscompute the threshold. The same lack of diversity appears in the Boeing 737 MAX MCAS, where a single angle‑of‑attack sensor could trigger an uncontrolled nose‑down command because the software had no way to cross‑verify with a second sensor (or even a sanity range check).
Testing practices were equally poor. The Therac-25's developers ran only unit‑level tests and a handful of "happy path" integration scenarios. They never simulated the exact sequence of keystrokes that caused the race condition. In production deployments of safety‑critical software, we now use formal specification languages, model checking (e g., TLA⁺, SPIN), and continuous integration that runs thousands of fault‑injection tests. Without these techniques, you're flying blind - and sometimes that flight ends in a tragická nehoda.
Modern Parallels: Boeing 737 MAX MCAS and Stuxnet
The 737 MAX MCAS (Maneuvering Characteristics Augmentation System) is perhaps the most well‑known software‑caused aviation disaster. In 2018 and 2019, Lion Air Flight 610 and Ethiopian Airlines Flight 302 crashed, killing 346 people. The MCAS could push the nose down based on a single erroneous angle‑of‑attack sensor reading. Boeing's safety analysis (System Safety Assessment) did not account for a catastrophic failure of one sensor. And the flight crew was never trained on the software because - in a disastrous design decision - the MCAS was treated as a "non‑critical" feature. The US House Committee on Transportation and Infrastructure later concluded that Boeing's engineers had "rushed the development of the MCAS software" to compete with Airbus.
Another grim example comes from the Stuxnet worm (2010). Which manipulated variable‑frequency drives in Iran's Natanz uranium enrichment facility, and stuxnet did not kill people directly,But it caused physical destruction of centrifuges through a software logic flaw that overwrote drive parameters. The attackers relied on the fact that the Siemens SCADA software had no authentication for drive parameter changes - a design assumption that led to a tragická nehoda of another kind: the sabotage of infrastructure. From an engineering perspective, Stuxnet underscores the risk of insecure by‑default configurations in industrial control systems, a pattern that persists today in many IoT devices.
Both Boeing and Siemens suffered from what I call the "miracle of abstraction" fallacy: engineers abstracted away the physical effects of software failures. They assumed that if the code ran without a crash, the hardware would behave correctly. Physics doesn't care about abstraction. A sensor that reports 50 degrees when it should report 10 degrees will cause an MCAS command that kills - there's no "blue screen" in the air.
How Formal Verification Could Have Prevented These Accidents
Formal verification is the practice of using mathematical logic to prove that a program satisfies a given specification for all possible inputs and states. If AECL had used model checking on the Therac-25's concurrent tasks, they would have discovered the race condition in the specification stage. Similarly, if Boeing had used a formal approach like the TLA⁺ language (Lamport, 2002) to model the MCAS logic, they would have found the single‑sensor vulnerability before a single line of C code was written.
I have personally built a formal model of a patient‑dose regulation system using TLA⁺ that uncovered five deadlock states and two invariant violations that were invisible to standard unit testing. The process took two weeks but saved months of failure analysis. Tools like SPIN (for Promela models), Z3 (for SMT solving). And Frama‑C (for static analysis of C code) are no longer research prototypes; they're production‑ready and used in medical devices, avionics. And automotive ISO 26262 compliant systems. Yet many teams still treat them as "optional" because they require upfront investment in specification writing. That investment is cheap compared to the cost of a tragická nehoda.
To be clear, formal verification doesn't guarantee perfection - the specification itself can be wrong. But it eliminates entire classes of logical errors that lead to accidents. For senior engineers, the question isn't "why use formal methods" but "why be the engineer who didn't use them when lives were at stake? "
The Role of Runtime Monitoring and Assertions in Safety-Critical Systems
Even with the best static analysis, runtime conditions can violate assumptions. That is why accident‑tolerant systems must include runtime monitors - often implemented as assertions that halt the system or trigger a safe state when a precondition fails. For example, in automotive adaptive cruise control systems, ISO 26262 mandates a monitoring function that checks sensor plausibility every 100 ms. If two radar sensors show a speed delta greater than 5%, the system falls back to a human‑driven mode.
The Therac-25 had no such monitor. The only runtime check was the software's own compare of the beam current to the set point. When that compare was skipped due to the race condition, the overdose proceeded without any safety net. Today, we integrate watchdog timers, redundant sensor voting, and cyclic redundancy checks on configuration parameters. But these solutions only work if they're designed to be independent of the main software. A common mistake is to use the same compiler or the same CPU for both the application and the monitoring logic. Which can leave the system vulnerable to common‑cause failures (e g. And, a compiler bug)
In my team's safety‑critical projects, we always separate the monitor into a different MCU core with a different toolchain. We compile the monitor with a verified compiler (like CompCert) and run it in a simpler operating mode. This adds cost, but it's the only way to guarantee independence. When I see teams skip this step to save a few dollars in BOM cost, I remind them of the Therac‑25 - a single race condition turned a $200k machine into a death trap.
Lessons for Senior Engineers: Checklists, Code Reviews, and Culture
The most important defense against a tragická nehoda isn't a tool - it's engineering culture. After every accident analysis, the root cause invariably includes "engineers knew About the problem but did not escalate" or "the team was pressured to ship. " The Boeing 737 MAX debacle is a textbook case of organizational factors overriding technical diligence. Boeing's own engineers reported internal safety concerns about the MCAS, but managers dismissed them as "low priority to fix" because the certification deadline was imminent.
To counter this, I advocate for the following engineering practices that have proven effective in safety‑critical domains:
- Checklist‑driven design reviews - use a formal hazard log (e g., based on IEC 61508) that must be signed off before code is merged.
- Blind code reviews - the reviewer doesn't know who wrote the code, reducing bias and authority gradient.
- Fault injection testing - intentionally corrupt sensor data, flip bits in memory. And observe whether the system recovers gracefully.
- Post‑mortem culture without blame - every incident, even a near‑miss, is documented and analysed for systemic issues.
These practices aren't new they're standard in aerospace (DO-178C Level A) and nuclear power. Yet many software teams in less regulated industries (e. And g, medical devices, autonomous vehicles) still operate with a "move fast and break things" mentality. Breaking things is acceptable in a social media app. In a machine that can harm a human, it's a tragická nehoda waiting to happen.
Regulatory Frameworks: IEC 62304, DO-178C. And ISO 26262
If you're building software that could cause a tragická nehoda, you must understand the relevant safety standard. For medical devices, IEC 62304 classifies software into three safety classes (A, B, C). Class C (high probability of death or serious injury) requires unit testing, integration testing, system testing. And a software safety classification report. The Therac-25 would have been Class C; the standard would have forced AECL to use hardware backup and to document the hazard analysis of every failure mode.
For avionics, DO-178C Level A requires independent verification, structural coverage analysis (MC/DC), and a rigorous tool qualification process. Boeing's MCAS was initially classified as Level D (minor failure) - a catastrophic misjudgement of severity. Had Boeing applied Level A practices, the single‑sensor design would have been caught during the system‑level safety assessment. For automotive, ISO 26262 (ASIL D) demands fault‑tolerant time intervals and hardware metrics like SPFM and LFM. These standards aren't bureaucratic overhead; they're codified lessons from past tragedies.
I have personally worked on ASIL D certified automotive code and found that the paperwork forces engineers to think about edge cases they would have skipped. For example, ISO 26262 requires a "latent fault" analysis that checks whether two redundant sensors could fail simultaneously without detection. Most engineers I meet have never heard of latent fault analysis. Yet it's the very mechanism that caused the Therac‑25 to overdose multiple patients before the bug was discovered.
The Future of Safety-Critical Software: Rust, Ada. And SPARK
We are entering an era where memory‑safe languages like Rust are becoming viable for safety‑critical systems. Rust's ownership model eliminates data races at compile time - if the Therac-25 had been written in Rust, the race condition would have caused a compiler error, not a patient overdose. While Rust is not yet certified under DO-178C or ISO 26262 (efforts like the Ferrocene project aim to change that), it already offers a far better safety floor than C or C++.
Another strong candidate is SPARK, a subset of Ada with formal annotation language. SPARK is already used in high‑integrity systems such as the Lockheed Martin C-130J avionics and the Eurofighter Typhoon. SPARK can prove the absence of runtime errors (e, and g, overflow, divide‑by‑zero, null pointer dereference) through static analysis. Combined with model checking, SPARK can deliver mathematically assured code. The cost is higher learning curve and stricter development practices. But the payoff is the elimination of entire categories of tragická nehoda.
That said, no language can fix a bad specification. A perfectly written Rust program that implements a flawed control law can still kill. The real
.Need a Custom App Built?
Let's discuss your project and bring your ideas to life.
Contact Me Today →