Skip to main content
postmortem

The Mechanism

6 min read Chapter 3 of 38

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:

StepTaskActiontreatment_modebeam_energyturntable
1keyboardOperator types ‘X’ (photon)1
2setupReads mode=1, sets energy=25 MeV125moving to photon
3setupCommands turntable to photon position125photon
4keyboardOperator changes to ‘E’ (electron)025photon
5setupReads mode=0, sets energy=5 MeV05moving to electron
6keyboardOperator presses SET quickly05moving
7beam_controlReads setup_complete=105moving
turntable reaches electron position05electron
8beam_controlFires 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:

StepTaskActiontreatment_modebeam_energyturntable
1keyboardOperator types ‘X’ (photon)1
2setupReads mode=1, sets energy=25 MeV125moving to photon
3setupCommands turntable to photon position125photon
4keyboardOperator quickly changes to ‘E’025photon
5keyboardOperator immediately presses SET025photon
6beam_controlReads setup_complete=1, mode=0025photon
7setupHas not run yet. Energy still 25 MeV.025photon
8beam_controlTurntable begins moving to electron025moving
9Turntable reaches electron position025electron
10beam_controlPosition 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.