The Mechanism
The Mechanism
The Therac-25 software runs on a PDP-11/23 minicomputer. The operating system is a real-time executive written by the same programmer who wrote the application code. The executive provides a simple cooperative multitasking scheduler: tasks run until they voluntarily yield, at which point the scheduler selects the next task. There is no preemption. There are no semaphores. There is no mutual exclusion mechanism. Shared variables are accessed by any task at any time with no synchronization.
The following code is reconstructed from the analysis published by Nancy Leveson and Clark Turner, who examined the assembly source. The reconstruction uses C to make the logic readable, but the structure and the shared variable access pattern are faithful to the original.
// RECONSTRUCTED FROM INCIDENT REPORT (Leveson & Turner, 1993)
// Original was PDP-11 assembly language
// Shared global variables - no synchronization
volatile int treatment_mode; // 0 = electron, 1 = photon
volatile int beam_energy; // MeV setting
volatile int turntable_position; // physical position of target/filter
volatile int setup_complete; // flag set when configuration is done
// Task 1: Keyboard handler
// Runs when operator types at VT100 terminal
void keyboard_handler() {
int key = read_terminal();
if (key == 'E') {
treatment_mode = 0; // Set mode to electron
// FAILURE POINT: mode is updated but beam_energy is not
// adjusted here. Energy adjustment happens in setup_task.
}
if (key == 'X') {
treatment_mode = 1; // Set mode to photon
}
if (key == SET_KEY) {
setup_complete = 1; // Signal that operator has confirmed
}
yield(); // Return control to scheduler
}
// Task 2: Setup task
// Configures beam parameters based on treatment_mode
void setup_task() {
while (1) {
if (treatment_mode == 1) { // Photon mode
beam_energy = 25; // 25 MeV for photon
command_turntable(PHOTON_POSITION); // Target + filter in path
} else { // Electron mode
beam_energy = 5; // 5 MeV for electron
command_turntable(ELECTRON_POSITION); // Target + filter out
}
yield(); // Return control to scheduler
}
}
// Task 3: Beam control
// Fires when setup_complete is set
void beam_control() {
while (1) {
if (setup_complete == 1) {
// FAILURE POINT: Reads turntable_position and beam_energy
// but these may reflect different mode configurations
// if the operator changed mode between setup_task runs
if (turntable_position == expected_position(treatment_mode)) {
fire_beam(beam_energy);
// If treatment_mode says electron but beam_energy
// is still 25 MeV from a previous photon setup,
// this fires 25 MeV with no target in the beam path
}
setup_complete = 0;
}
yield();
}
}
The race condition exists because three tasks share mutable state with no synchronization and the scheduler provides no atomicity guarantees for multi-variable updates.
The specific interleaving that produces the overdose:
| Step | Task | Action | treatment_mode | beam_energy | turntable |
|---|---|---|---|---|---|
| 1 | keyboard | Operator types ‘X’ (photon) | 1 | — | — |
| 2 | setup | Reads mode=1, sets energy=25 MeV | 1 | 25 | moving to photon |
| 3 | setup | Commands turntable to photon position | 1 | 25 | photon |
| 4 | keyboard | Operator changes to ‘E’ (electron) | 0 | 25 | photon |
| 5 | setup | Reads mode=0, sets energy=5 MeV | 0 | 5 | moving to electron |
| 6 | keyboard | Operator presses SET quickly | 0 | 5 | moving |
| 7 | beam_control | Reads setup_complete=1 | 0 | 5 | moving |
| — | … | turntable reaches electron position | 0 | 5 | electron |
| 8 | beam_control | Fires at 5 MeV, electron mode. Safe. |
That is the safe interleaving. The operator was slow enough that the setup task completed its reconfiguration before the beam fired.
The lethal interleaving:
| Step | Task | Action | treatment_mode | beam_energy | turntable |
|---|---|---|---|---|---|
| 1 | keyboard | Operator types ‘X’ (photon) | 1 | — | — |
| 2 | setup | Reads mode=1, sets energy=25 MeV | 1 | 25 | moving to photon |
| 3 | setup | Commands turntable to photon position | 1 | 25 | photon |
| 4 | keyboard | Operator quickly changes to ‘E’ | 0 | 25 | photon |
| 5 | keyboard | Operator immediately presses SET | 0 | 25 | photon |
| 6 | beam_control | Reads setup_complete=1, mode=0 | 0 | 25 | photon |
| 7 | setup | Has not run yet. Energy still 25 MeV. | 0 | 25 | photon |
| 8 | beam_control | Turntable begins moving to electron | 0 | 25 | moving |
| 9 | — | Turntable reaches electron position | 0 | 25 | electron |
| 10 | beam_control | Position matches mode. Fires at 25 MeV. OVERDOSE. |
The difference between life and death is whether the setup task runs between step 4 and step 6. If the operator edits the mode and presses SET within the same scheduler cycle, the setup task never gets a chance to adjust the beam energy. The beam fires at 25 MeV in electron mode: full energy, no target, no filter.
The timing window is approximately 8 seconds, the time it takes the turntable to rotate. An operator who corrects the mode and presses SET within those 8 seconds, before the setup task runs and the turntable completes its movement, can trigger the overdose. Experienced operators, who type quickly and correct errors efficiently, are more likely to hit this window than novices who type slowly and hesitate.
There is a second, related bug. A counter variable in the setup routine is stored in a single byte. It increments on every pass through the setup check loop. When it overflows from 255 to 0, the setup check is skipped entirely for one cycle. This means that even an operator who edits slowly can trigger the overdose if her edit happens to coincide with the counter overflow. The probability is roughly 1 in 256 per edit, which is low enough to be invisible in testing and high enough to be inevitable across thousands of treatments.
The machine has no independent check. No hardware interlock verifies that the beam energy matches the turntable position at the moment of firing. The software is the only safety layer, and the software has a race condition.