Therac-25: коли збої програмного забезпечення вбивали пацієнтів

Було корисно?

У виробничих середовищах відмови зазвичай бувають «лише» дорогими. Дзвінок на пейджер, провал доходів, клієнт кричить — ми виправляємо й продовжуємо.
Therac-25 був іншим: сукупність рішень у програмному забезпеченні та системному дизайні перетворила медичний прилад на машину, що могла видати
катастрофічну передозу радіації — і при цьому впевнено повідомляти оператору, що все в порядку.

Якщо ви будуєте сервіси, платформи, сховища чи будь-що, що виводить код у світ, ця справа не є древньою історією.
Це польовий звіт про те, що відбувається, коли ви прибираєте апаратні блокування, довіряєте конкурентності, якої не розумієте,
і ставитеся до інцидентів як до «помилок користувача», поки фортуна не відвернеться.

Що таке Therac-25 і чому це важливо

Therac-25 — комп’ютеризований медичний лінійний прискорювач, що використовувався для променевої терапії в середині 1980-х.
Він міг працювати в кількох режимах, включно з електронним пучком і високоюенергетичним рентгенівським режимом. У безпечній роботі
апаратне та програмне забезпечення повинні координуватися, щоб гарантувати, що енергія пучка й фізична конфігурація збігаються — ціль,
фільтри, коліматори та вся нудна механіка, що тримає пацієнтів живими.

Сутність «чому це важливо» не в тому, що в ПЗ були баги. У всьому є баги. Історія в тому, що дизайн пристрою покладався на ПЗ, щоб
забезпечити гарантії безпеки, які старіші машини реалізовували апаратними блокуваннями.
Коли ПЗ помилялося — а воно помилялося тонко й залежно від таймінгів — машина могла видати величезну передозу
без правдоподібного тривожного сигналу чи надійного відмовостійкого механізму.

Якщо ви колись замінили фізичну огорожу безпеки на «логіку в контролері», ви торкалися тієї самої розжареної плити.
Якщо ви колись прибрали «повільну» перевірку, бо «це ніколи не трапляється», ви стояли в тому самому вибуховому радіусі.
І якщо ви колись сприймали звіт оператора як шум, бо «система каже OK», — ви вже тримаєте сірник.

Конкретні факти та історичний контекст (коротко, корисно, не для вікторини)

  • Інциденти Therac-25 сталися в середині 1980-х, з кількома випадками передозування, пов’язаними з помилками ПЗ і системним дизайном.
  • Раніші суміжні машини (Therac-6 і Therac-20) використовували більше апаратних блокувань; Therac-25 більше покладався на ПЗ для безпеки.
  • Оператори могли викликати небезпечний стан через швидкі послідовності введення (поведінка залежна від таймінгів), класичний симптом умов гонки/конкурентності.
  • Повідомлення про помилки були незрозумілими (наприклад, розмиті коди «malfunction»), що спонукало персонал до повторних спроб замість безпечного вимкнення та ескалації.
  • Деякі передозування спочатку списували на реакції пацієнтів або помилки оператора, що затримувало ефективну локалізацію та виправлення причин.
  • Повторно використане ПЗ вважали безпечним, тому що воно було в попередніх продуктах, хоча модель безпеки змінилася зі зменшенням апаратних блокувань.
  • Логування та діагностика були недостатніми для швидкої реконструкції подій — подарунок для заперечення і прокляття для реагування на інциденти.
  • Регуляторні та індустріальні практики безпеки ПЗ були менш сформовані, ніж нині; формальні методи та незалежна V&V застосовувалися не скрізь.
  • Цей випадок став основоположним уроком з безпеки ПЗ, який використовують десятиліттями в етиці інженерії та навчанні надійності.

Два факти мають вас непокоїти більше: «повторне використання ПЗ вважалося безпечним» і «незрозумілі помилки, що підштовхували до повторних спроб».
Це 2026 рік у іншому вбранні.

Ланцюжок відмов: від натискання клавіш до летальної дози

1) Безпека перемістили з апаратури в програмне забезпечення, але ПЗ не будували як систему безпеки

У системі критичній для безпеки ви не можете сказати «код має робити X». Ви повинні сказати «система не може робити Y»,
навіть при часткових відмовах: зависли біти, аномалії таймінгу, несподівані послідовності, некоректні входи, деградовані датчики, стрибки живлення.
Апаратні блокування історично примушували переходити в безпечний стан незалежно від програмної плутанини.

Therac-25 зменшив кількість апаратних блокувань і поклався на координацію в ПЗ. Це рішення не є автоматично неправильним,
але воно підвищує вимоги до доведення безпеки. Якщо ви прибираєте фізичні обмеження, ваше ПЗ має бути інженерно, верифіковано
та моніторено так, ніби воно тримає єдиний парашут.

2) Умови гонки: ті, що сміються над вашим планом тестування

Сумнозвісний патерн у дискусіях про Therac-25 — певні швидкі послідовності дій оператора могли привести систему в невірний
внутрішній стан. Оператор міг швидко редагувати параметри лікування, а система могла прийняти нові значення, поки інші частини логіки
ще припускали старі значення. Тепер у вас «енергія пучка встановлена для режиму A» і «механічна конфігурація налаштована для режиму B».
Ця невідповідність — не просто «баг». Це летальна конфігурація.

Умови гонки — це збої координації. Вони пролазять повз unit-тести, ховаються від нормальних робочих сценаріїв і «люблять» швидких операторів,
тому що люди дивовижно вміють створювати дивні таймінги. Сучасний еквівалент — розподілена система, яка «зазвичай сходиться», але час від часу
записує застарілу конфігурацію не тим клієнтам. У безпекових контекстах «іноді» неприйнятно.

3) UI і повідомлення про помилки навчали персонал продовжувати роботу

Щоб система відмовляла безпечно, ви повинні навчити людей навколо неї, як виглядає «небезпечно». В Therac-25 операторам, як повідомляють,
виводилися заплутані коди помилок. Пристрій міг показати помилку, дозволити оператору продовжити після підтвердження і не ясно вказати
ступінь серйозності чи правильну дію.

UI для безпеки не оптимізує пропускну здатність. Він оптимізує правильну ескалацію. Він використовує просту мову,
явну серйозність і однозначні обов’язкові дії. Коли UI робить «повторну спробу» найпростішим шляхом,
це вже не помилка користувача, коли люди повторюють спроби. Це проєктування.

4) Слабка спостережуваність дозволяла заперечувати проблему

Після інциденту вам потрібна криміналістика: логи, знімки стану, показники датчиків, послідовності команд, часові мітки
та спосіб кореляції між ними. Діагностика Therac-25 не дозволяла швидко і впевнено відтворити події.
За відсутності доказів організації за замовчуванням обирають наратив. Найпоширеніший — «помилка оператора».

Спостережуваність — це контроль, а не розкіш. У системах критичних для безпеки це також моральний обов’язок:
ви не можете виправити те, чого не бачите, і ви не можете довести безпеку лише на відчуттях.

5) Реагування на інциденти трактували як аномалію, а не як сигнал

Одна передозування має спричинити повну зупинку: локалізацію, повідомлення про безпеку, незалежне розслідування і ухил
у бік «система винна, поки не доведено протилежне». Натомість історична картина — затримана ідентифікація та повторна шкода.

Це впізнаваний організаційний режим відмов: розглядати інцидент як поодинокий «дивний випадок», локально виправити та рухатися далі.
Проблема в тому, що другий інцидент уже не «дивний». Це ваша система, що викриває істину гучніше.

6) Безпека — це властивість системи; Therac-25 оптимізували як продукт

Люди іноді підсумовують Therac-25 як «баг у ПЗ убив пацієнтів». Це неповне й дещо заспокійливе формулювання,
через що воно й прижилося. Глибша історія в тому, що підвели одразу кілька шарів:
проєктні припущення, відсутні блокування, дефекти конкурентності, слабка діагностика та культура реагування, яка не трактувала
повідомлення як термінові докази.

Тут пасує одна цитата, бо вона розрізає багато сучасного нісеніття:
парафраз ідеї: «надія — не стратегія.» — генерал Gordon R. Sullivan (часто цитують у контекстах надійності й планування).

Замініть «надія» на «має бути», «зазвичай» або «не може статись», і ви отримаєте надійний генератор постмортемів.

Жарт №1: Умови гонки як таргани — якщо ви побачили одного в продакшні, припустіть, що за робочим середовищем їх сорок ще ховаються за «в мене працює локально».

Уроки для систем: що копіювати, а що ніколи не повторювати

Урок A: Не прибирайте блокування, якщо не заміните їх на сильніші гарантії

Апаратні блокування — грубі інструменти, але вони неймовірно ефективні. Вони відмовляють передбачувано і їх важко випадково обійти.
Програмні блокування гнучкі, але гнучкість — не те саме, що безпека.
Якщо ви переносите безпеку в ПЗ, підвищуйте дисципліну інженерії:
формальний аналіз ризиків, явні вимоги безпеки, незалежна верифікація, тестові стенди, що намагаються ламати таймінги,
і моніторинг виконання, який передбачає, що код може помилятися.

У термінах SRE: не видаляйте ваші circuit breakers лише тому, що «новий service mesh має retries».

Урок B: Розглядайте таймінг і конкурентність як загрози першого класу

Найвідоміші режими відмов Therac-25 залежать від таймінгу. Основний оперативний урок: тестуйте систему
під агресивними таймінговими умовами: швидкі входи, затримані датчики, перебудова, часткові оновлення, застарілі кеші та повільні периферії.
Якщо ви не можете змоделювати це — фузьте. Якщо не можете фузити — ізолюйте з жорсткими обмеженнями.

У сучасних платформах: якщо ваша безпека залежить від «цей обробник подій не буде перезапущений», ви ставите життя на коментар.
Використовуйте локи, ідемпотентність, автомати станів з явними переходами та інваріанти, які перевіряються під час виконання.

Урок C: UI — частина контуру безпеки

Інтерфейси мають робити безпечну дію простою, а небезпечну — складною. Повідомлення про помилки мають бути дієвими. Тривоги мають мати градацію.
І система не повинна показувати «усе гаразд», коли вона не може цього довести.

Якщо ваша система невпевнена, вона має сказати «я не впевнений» і за замовчуванням перейти в безпечний стан. Інженерам це не подобається, бо це «консервативно».
Пацієнтам це подобається — вони залишаються живими, щоб поскаржитися на вашу консервативну проєктну політику.

Урок D: Постмортеми — це контролі безпеки, а не бюрократія

Therac-25 — також про іституційне навчання. Коли трапляються інциденти, потрібен процес, що витягує сигнал:
безосудовий тон, безжальна аналітика та орієнтація на дії.
«Ми перенавчили операторів» — не виправлення, коли система дозволила летальні невідповідності станів. Це паперова косметика.

Урок E: Повторне використання не безкоштовне; повторно використаний код успадковує нові зобов’язання

Повторне використання коду може бути розумним. Але це також спосіб пронести старі припущення в нове середовище.
Якщо Therac-25 використав код з систем, що мали апаратні блокування, то неявна модель безпеки коду змінилася.
У термінах надійності: ви змінили граф залежностей, але залишили старий SLO.

Коли повторно використовуєте, ви мусите повторно валідувати safety case. Якщо ви не можете викласти safety case простою мовою і захистити його під перехресним допитом,
то у вас немає safety case.

Три корпоративні міні-історії (анонімізовано, правдоподібно й на жаль поширено)

Міні-історія 1: Хибне припущення, що знищило білінг (і майже довіру)

Середня компанія вела «простий» event-пайплайн: API пише в чергу, воркери обробляють, база даних зберігає результати.
Система працювала стабільно рік, а це знак, що скоро вона стане всім предметом ненависті на тиждень.

Старший інженер зробив розумне припущення: ID повідомлень були глобально унікальними. Насправді вони були унікальні по партиції, а не глобально.
Код використовував message ID як ключ ідемпотентності в спільному Redis. При нормальному навантаженні колізії були рідкісні.
Під час спайку трафіку колізії перестали бути рідкісними.

Результатом були тихі пропуски: воркери вважали, що повідомлення вже оброблене, і пропускали його. Події білінгу зникли.
Клієнтів не стягували більше; їм не нараховували. Це весело доти, поки фінанси не приходять з таблицями й питаннями.
Моніторинг цього не помітив, бо рівні помилок залишалися низькими. Система була «здоровою», поки коректність горіла.

Постмортем був чесним: припущення не документоване, не протестоване й не простежене. Виправили, обмеживши ключі ідемпотентності
партицією+зсувом, додавши інваріанти й створивши job для звірки пропусків. Також додали canary-трафік, який навмисно створював колізії,
щоб перевірити виправлення в умовах, наближених до продакшену.

Урок Therac-25 тут проявився: коли припущення хибні, система не завжди падає. Іноді вона посміхається й брешіть.

Міні-історія 2: Оптимізація, що обернулася проти команди (бо «швидко» не є вимогою)

Інша організація мала аналітичний сервіс на сховищі. Щоб зменшити латентність, вони додали локальний кеш на кожному вузлі
і прибрали «надлишкову» перевірку контрольної суми при читанні блобів з об’єктного сховища. Чексума була дорогою,
стверджували вони, а шар збереження «вже гарантує цілісність».

Місяцями все виглядало чудово. Латентність нижча, CPU менше, графіки вверх. Потім оновлення ядра привнесло рідкісну
проблему корупції даних, пов’язану з DMA, на одному типі інстансів. Це було рідко, важко відтворюване, і об’єктне сховище тут ні до чого.
Корупція відбувалася між пам’яттю та юзерлендом при читаннях, а видалена чексума була єдиною практичною точкою виявлення.

Впали не лише дані; провалилося реагування на інцидент. Команда ганялася за «поганими запитами» та «дивними клієнтами»
кілька днів, бо їхні дашборди показували успіх. Корупція була семантичною, а не транспортною.
Зрештою знайшли її, порівнявши результати між вузлами і помітивши, що один вузол «творчо неправий».

Відкат був принизливим, але ефективним: знову ввімкнули чексуми, додали валідацію між вузлами в Canary,
і запустили періодичні scrub-завдання. Пізніше оптимізацію повернули з запобіжниками: чексуми вибірково перевірялися з високою частотою,
а для критичних даних була примусова повна валідація.

Therac-25 прибрав шари, які робили небезпечні стани важчими для досягнення. Це була та сама помилка, тільки з меншими похованнями і більше дашбордами.

Міні-історія 3: Нудна практика, що врятувала день (і ніхто за це не отримав підвищення)

Платформа платежів мала практику, яку інженери глузливо називали «параноїдальною»: кожне релізне розгортання вимагало поетапного rollout
з автоматично перевіреними інваріантами на кожному етапі. Не лише помилки й латентність — бізнес-інваріанти: підсумки, лічильники, звірка з незалежними джерелами.

Одного п’ятничного пополудня (бо, звісно) новий реліз приніс тонкий баг подвійного застосування, що тригерився логікою повторів
при тайм-ауті downstream-сервісу. У інтеграційних тестах цього не було, бо тести не змоделювали тайм-аути реалістично.
Баг проявився в першому canary-cell з реальним трафіком.

Інваріанти спіймали його за кілька хвилин: підсумки бухгалтерії canary-клітини відхилилися від контрольної клітини.
Rollout зупинився автоматично. Жодного повного outage. Жодного впливу на клієнтів, окрім кількох транзакцій, які були автоматично скасовані до розрахунку.

Виправлення було простим: токени ідемпотентності перевели з «найкращого зусилля» в «обов’язкові», а шлях повтору змінили так,
щоб перевіряти статус коміту перед повтором. Головна ідея — спрацював процес, а не героїчні подвиги.
Нікого не вітали стоячи. Усі пішли додому.

У Therac-25 не було такого інваріантно-орієнтованого стейджингу, і результатом була повторна шкода, поки шаблон не прийняли.
Нудне — добре. Нудне дозволяє вижити.

Швидкий план діагностики: що перевіряти першим/другим/третім

Операційна катастрофа Therac-25 була посилена повільною, неоднозначною діагностикою. У виробничих системах швидкість важлива — але не типу
«швидко хаотично змінювати речі». Мова про «швидко встановити режим відмови».

Перший: вирішіть, чи система брешe, чи відмовляє голосно

  • Шукайте невідповідність між «здоровими» сигналами і повідомленнями про шкоду від користувачів. Якщо користувачі бачать корупцію, неправильні результати чи небезпечну поведінку, вважайте дашборди недостовірними.
  • Перевіряйте інваріанти. Підрахунки, підсумки, переходи станів, інтерлоки безпеки. Якщо у вас нема інваріантів — вітаємо: у вас «відчуття».
  • Підтвердіть радіус ураження. Один вузол? Один регіон? Одна пристрій? Один робочий процес оператора?

Другий: ізолюйте конкурентність, стан і таймінг

  • Підозрюйте умови гонки, коли: симптоми переривчасті, тригеряться швидкістю або зникають при додаванні логування.
  • Тимчасово примусьте серіалізацію (один воркер, відключити паралелізм, зафіксувати критичну секцію), щоб побачити, чи зникає проблема.
  • Захоплюйте порядок подій з часовими мітками та correlation IDs. Якщо не відтворити порядок — неможливо мислити про безпеку.

Третій: перевірте обгортку безпеки та відмовостійку поведінку

  • Протестуйте перехід у «безпечний стан». Система зупиняється при невизначеності, чи продовжує роботу?
  • Перевірте інтерлоки: апаратні, програмні, конфігураційні гейтінги, feature flags, circuit breakers.
  • Підтвердіть керівництво для операторів: чи повідомлення про помилку навчають правильних дій, чи вони тренують повторні спроби?

Жарт №2: Якщо ваш інцидентний рукопис каже «перезапустіть і подивіться», це не рукопис — це дошка Вуду з кращим аптаймом.

Практичні завдання: команди, виводи та рішення, які з них випливають

Therac-25 не впав тому, що хтось забув Linux-команду. Але операційна тема — недостатні докази, повільна діагностика та відсутні інваріанти —
зустрічається скрізь. Наведені нижче завдання можна виконати на реальних системах, щоб ловити сучасні еквіваленти:
умови гонки, небезпечні переходи станів, «здорово, але неправильно» та видалення шарів безпеки.

Шаблон для кожного завдання: команда → типовий вивід → що це означає → яке рішення ви приймаєте.

Завдання 1: Підтвердіть синхронізацію часу (порядок має значення)

cr0x@server:~$ timedatectl status
               Local time: Mon 2026-01-22 10:14:07 UTC
           Universal time: Mon 2026-01-22 10:14:07 UTC
                 RTC time: Mon 2026-01-22 10:14:06
                Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

Що це означає: «System clock synchronized: yes» знижує ймовірність, що логи брешуть щодо порядку подій.

Рішення: Якщо не синхронізовано, виправте NTP/chrony перед тим, як довіряти мульти-вузловим часовим лініям.

Завдання 2: Перевірте на рівні ядра проблеми, що можуть імітувати «випадкову» корупцію

cr0x@server:~$ dmesg -T | tail -n 12
[Mon Jan 22 10:11:41 2026] nvme nvme0: I/O 42 QID 3 timeout, aborting
[Mon Jan 22 10:11:41 2026] nvme nvme0: Abort status: 0x371
[Mon Jan 22 10:11:43 2026] EXT4-fs (nvme0n1p2): I/O error while writing superblock
[Mon Jan 22 10:11:44 2026] blk_update_request: I/O error, dev nvme0n1, sector 91827364 op 0x1:(WRITE) flags 0x0 phys_seg 1 prio class 0

Що це означає: Затримки та помилки файлової системи можуть спричиняти тихі неправильності вище по стеку.

Рішення: Не звинувачуйте код додатка, поки не підтверджено стабільність апаратного забезпечення/драйверів; якщо помилки тривають — злити вузол.

Завдання 3: Визначте навантаження CPU проти латентності з інших джерел

cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.8.0 (prod-app-17) 	01/22/2026 	_x86_64_	(32 CPU)

10:14:22 AM  CPU   %usr %nice %sys %iowait %irq %soft %steal %idle
10:14:23 AM  all   18.2  0.0   6.1   22.7    0.0  0.5    0.0   52.5
10:14:23 AM   7   88.9  0.0   9.1    0.0    0.0  0.0    0.0    2.0

Що це означає: Високе %iowait вказує, що CPU просто чекають вводу/виводу; один «гарячий» CPU натякає на утруднення від локів або одно-потокову точку.

Рішення: Якщо iowait високий — перевірте збереження/мережу; якщо один CPU завантажений — підозрюйте точки серіалізації і mutex-утримання.

Завдання 4: Перевірте, чи система не треше пам’ять (таймінгові баги люблять тиск)

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  1      0  31248  41216 829320    0    0  1024  2048  980 2100 17  6 55 22  0
 6  3      0  28992  40128 821004    0    0  4096  8192 1200 8900 25  8 40 27  0

Що це означає: Висока кількість перемикань контексту (cs) разом з ростом заблокованих процесів (b) часто сигналізує про контенцій або I/O-стали.

Рішення: Якщо тиск високий — зменшіть паралелізм і перевірте знову; таймінг-залежні баги часто зникають при зниженні навантаження, що діагностично корисно.

Завдання 5: Підтвердіть здоров’я файлової системи та опції монтування (запобіжні елементи важливі)

cr0x@server:~$ mount | grep " /var "
/dev/nvme0n1p2 on /var type ext4 (rw,relatime,errors=remount-ro)

Що це означає: errors=remount-ro — це запобіжник: файлову систему переведуть у режим лише для читання при серйозних помилках замість тихої корупції.

Рішення: Віддавайте перевагу fail-safe опціям монтування для критичного стану; якщо бачите надто лояльні опції — виправте їх до того, як вони знадобляться.

Завдання 6: Перевірте деградацію RAID / mdadm (пастка «все гаразд»)

cr0x@server:~$ cat /proc/mdstat
Personalities : [raid1]
md0 : active raid1 sda1[0] sdb1[1]
      104320 blocks super 1.2 [2/1] [U_]
      [====>................]  recovery = 23.4% (24448/104320) finish=2.1min speed=64000K/sec

Що це означає: [2/1] [U_] вказує на деградоване дзеркало. Ви в одному диску від серйозних проблем.

Рішення: Ставте деградовану надлишковість як інцидент; призупиніть ризикові зміни, замініть збійний диск і перевірте завершення ребілда.

Завдання 7: Перевірте статус пулу ZFS (scrub, errors, silent corruption)

cr0x@server:~$ sudo zpool status -v tank
  pool: tank
 state: DEGRADED
status: One or more devices has experienced an error resulting in data corruption.
action: Restore the file in question if possible.
  scan: scrub repaired 0B in 00:12:44 with 1 errors on Mon Jan 22 09:58:13 2026
config:

	NAME        STATE     READ WRITE CKSUM
	tank        DEGRADED     0     0     0
	  mirror-0  DEGRADED     0     0     0
	    sdc     ONLINE       0     0     0
	    sdd     ONLINE       0     0     3

errors: Permanent errors have been detected in the following files:
/tank/db/segments/00000042.log

Що це означає: ZFS повідомляє, що виявлено помилки контрольних сум і може вказати на уражені файли.

Рішення: Відновіть уражені дані з реплік/резервних копій; не ставте «спостерігай і дивись». Якщо сховище каже про корупцію — вірте йому.

Завдання 8: Виявляйте TCP-переспрямування й втрати пакетів (невидимий атакувальний таймінг)

cr0x@server:~$ ss -s
Total: 1532
TCP:   812 (estab 214, closed 539, orphaned 0, timewait 411)

Transport Total     IP        IPv6
RAW	  0         0         0
UDP	  29        21        8
TCP	  273       232       41
INET	  302      253       49
FRAG	  0         0         0

Що це означає: Це грубий знімок; він не показує ретрансмісій прямо, але допомагає виявляти churn підключень і timewait-шторм.

Рішення: Якщо estab низький, але timewait величезний — підозрюйте агресивні патерни перепідключень; переходьте до глибших мережевих метрик.

Завдання 9: Перевірте помилки інтерфейсу і дропи

cr0x@server:~$ ip -s link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:ab:cd:ef brd ff:ff:ff:ff:ff:ff
    RX:  bytes packets errors dropped  missed   mcast
    9876543210 8123456      0   12456       0   12034
    TX:  bytes packets errors dropped carrier collsns
    8765432109 7123456      0     342       0       0

Що це означає: Дропи проявляються як повтори, тайм-аути і переупорядкування — ідеальне паливо для умов гонки і часткових оновлень.

Рішення: Якщо дропи ростуть — розслідуйте черги NIC, невідповідний MTU, затори та шумних сусідів; не налаштовуйте спочатку повтори на рівні додатка.

Завдання 10: Підтвердіть тиск на файлові дескриптори процесу (може спричиняти дивні відмови)

cr0x@server:~$ cat /proc/sys/fs/file-nr
42352	0	9223372036854775807

Що це означає: Перше число — виділені файлові дескриптори; якщо воно наближається до системних лімітів, ви отримаєте помилки, що виглядають «випадково».

Рішення: Якщо тиск високий — знайдіть процеси з витоком і виправте; також встановіть адекватні ліміти і сигналізуйте про темп росту.

Завдання 11: Дивіться, чи сервіс перезапускається (flapping ховає корінні причини)

cr0x@server:~$ systemctl status api.service --no-pager
● api.service - Example API
     Loaded: loaded (/etc/systemd/system/api.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2026-01-22 10:02:18 UTC; 11min ago
   Main PID: 2187 (api)
      Tasks: 34 (limit: 38241)
     Memory: 512.4M
        CPU: 2min 12.103s
     CGroup: /system.slice/api.service
             └─2187 /usr/local/bin/api --config /etc/api/config.yaml

Що це означає: Якщо Active постійно скидається — ви в crash-loop; якщо аптайм стабільний — шукайте інші джерела.

Рішення: Якщо flapping — зупиніть кровотечу: заморозьте деплої, зменшіть трафік і зберігайте core dumps/логи до перезапуску, щоб не втратити докази.

Завдання 12: Запитайте journald навколо підозрілої часової віконечка (отримати послідовність)

cr0x@server:~$ journalctl -u api.service --since "2026-01-22 09:55" --until "2026-01-22 10:10" --no-pager | tail -n 8
Jan 22 10:01:02 prod-app-17 api[2187]: WARN request_id=9b3e retrying upstream due to timeout
Jan 22 10:01:02 prod-app-17 api[2187]: WARN request_id=9b3e retry attempt=2
Jan 22 10:01:03 prod-app-17 api[2187]: ERROR request_id=9b3e upstream commit status unknown
Jan 22 10:01:03 prod-app-17 api[2187]: WARN request_id=9b3e applying fallback path
Jan 22 10:01:04 prod-app-17 api[2187]: INFO request_id=9b3e response_status=200

Що це означає: «Commit status unknown» із подальшим «fallback path» — червоний прапорець коректності: можливі подвійні застосування або частковий стан.

Рішення: Додайте верифікацію ідемпотентності перед повтором; якщо це критично для безпеки — закривайте доступ, а не «fallback і надіятися».

Завдання 13: Інспектуйте відкриті TCP-з’єднання до залежності (знайдіть гарячі точки)

cr0x@server:~$ ss -antp | grep ":5432" | head
ESTAB 0 0 10.20.5.17:48422 10.20.9.10:5432 users:(("api",pid=2187,fd=41))
ESTAB 0 0 10.20.5.17:48424 10.20.9.10:5432 users:(("api",pid=2187,fd=42))
ESTAB 0 0 10.20.5.17:48426 10.20.5.17:48426 10.20.9.10:5432 users:(("api",pid=2187,fd=43))

Що це означає: Підтверджує, що сервіс говорить з Postgres і не застряг в іншому місці. Також натякає на поведінку connection pool.

Рішення: Якщо підключень багато — обмежте пули і додайте backpressure; шторми підключень — це софтверний еквівалент видалення інтерлоку.

Завдання 14: Перевірте блокування в базі (вузькі місця конкурентності виглядають як «випадкова» повільність)

cr0x@server:~$ psql -h 10.20.9.10 -U app -d appdb -c "select wait_event_type, wait_event, count(*) from pg_stat_activity where wait_event is not null group by 1,2 order by 3 desc;"
 wait_event_type |     wait_event      | count
-----------------+---------------------+-------
 Lock            | relation            |    12
 IO              | DataFileRead        |     4
 Client          | ClientRead          |     2
(3 rows)

Що це означає: Багато relation locks вказує на контенцію; ваш еквівалент «швидкого оператора» може бути гарячою таблицею.

Рішення: Якщо очікування на блокування домінують — знайдіть блокуючі запити і виправте схему/індексацію або сферу транзакцій; не просто масштабуйтесь додатковими нодами.

Завдання 15: Перевірте налаштування контрольних сум/верифікації для реплікації сховища (перевірка запобіжника)

cr0x@server:~$ rbd info rbd/patient-images
rbd image 'patient-images':
	size 2 TiB in 524288 objects
	order 22 (4 MiB objects)
	snapshot_count: 2
	id: 1a2b3c4d5e6f
	block_name_prefix: rbd_data.1a2b3c4d5e6f
	format: 2
	features: layering, exclusive-lock, object-map, fast-diff, deep-flatten
	op_features:
	flags:
	create_timestamp: Mon Jan 15 08:21:11 2026

Що це означає: Це не сам вивід чексуми, але підтверджує характеристики об’єктів; вам усе одно потрібні наскрізні перевірки цілісності на рівні додатка.

Рішення: Якщо коректність системи залежить від цілісності, додайте явні контрольні суми/хеші на межах; не перекладайте істину лише на один шар.

Завдання 16: Підтвердіть, що процес однопотоковий або заблокований на лок

cr0x@server:~$ top -H -p 2187 -b -n 1 | head -n 12
top - 10:14:55 up 12 days,  3:21,  1 user,  load average: 6.12, 4.98, 4.77
Threads:  36 total,   1 running,  35 sleeping
%Cpu(s): 23.1 us,  7.2 sy,  0.0 ni, 47.3 id, 22.4 wa,  0.0 hi,  0.0 si,  0.0 st
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
   2199 api       20   0 1452784 524200  49200 R  88.7   1.6   0:31.22 api
   2201 api       20   0 1452784 524200  49200 S   2.3   1.6   0:02.10 api

Що це означає: Один потік домінує на CPU; решта сплять. Це часто свідчить про лок, tight loop або одну «гарячу» доріжку.

Рішення: Профілюйте цю доріжку; якщо вона захищає спільний стан, переробіть у бік незмінного стану або явних автоматів станів з безпечними переходами.

Поширені помилки: симптоми → корінна причина → виправлення

1) Симптом: «Система каже OK, але користувачі повідомляють про неправильні результати»

Корінна причина: Health checks вимірюють життєздатність (процес працює), а не коректність (інваріанти). Therac-25 фактично «прошов» власні перевірки, лишаючись небезпечним.

Виправлення: Додайте перевірки коректності: крос-валідацію, звірки та дашборди інваріантів. Гейтуйте релізи за інваріантами, а не лише за 200.

2) Симптом: Переривчасті збої, що тригеряться швидкістю, повторним спробами або великим навантаженням

Корінна причина: Умови гонки та неатомарні переходи станів. Еквівалент «швидкого оператора» — паралельні запити або перероблені події.

Виправлення: Моделюйте стан як скінченний автомат, забезпечте атомарні переходи, додайте ключі ідемпотентності і фузьте таймінги за допомогою chaos-тестів.

3) Симптом: Оператори неодноразово «повторюють» через помилки

Корінна причина: UI/UX навчає небезпечної поведінки; тривоги розмиті; система дозволяє продовжити без доказу безпеки.

Виправлення: Зробіть неможливим небезпечне продовження. Використовуйте явну серйозність і обов’язкові дії. Якщо безпека невизначена — відмовляйтесь.

4) Симптом: Післяінцидентне розслідування не може відтворити, що сталося

Корінна причина: Недостатнє логування, відсутні correlation IDs, нестача часових міток або логи перезаписуються після рестарту.

Виправлення: Структуровані логи з correlation IDs, незмінний append-only аудит, і знімки стану при збоях.

5) Симптом: Виправлення — «перенавчити користувачів» і «бути обережними»

Корінна причина: Організація замінює інженерні контроли політиками, бо інженерні контролі складніші.

Виправлення: Додайте жорсткі запобіжники: інтерлоки, пермишини на переходи, runtime-перевірки і незалежну верифікацію. Тренування — додаткове, а не головне.

6) Симптом: Видалення «надлишкових» перевірок робить усе швидшим… поки ні

Корінна причина: Оптимізація видалила шари виявлення/виправлення (чексуми, валідації, підтвердження читань), перетворивши рідкісні збої на тиху корупцію.

Виправлення: Зберігайте наскрізну валідацію; якщо оптимізуєте, вибірково пробуйте і додавайте canary-перевірки. Ніколи не оптимізуйте єдиний тривожний сигнал.

7) Симптом: Інциденти трактують як ізольовані аномалії, а не як патерни

Корінна причина: Слабке управління інцидентами: немає централізованого трекінгу, примусової ескалації, інцентиви занижувати серйозність.

Виправлення: Формальне реагування на інциденти з класифікацією серйозності, агрегацією по сайтах і правом «зупинити лінію», коли під загрозою безпека/коректність.

Чеклісти / покроковий план

Покроково: створення «Therac‑доказових» запобіжників у програмно-керованих системах

  1. Напишіть список небезпек серйозно.
    Перерахуйте небезпечні стани, а не лише відмови. Наприклад: «режим високої енергії з неправильною механічною конфігурацією» відповідає «привілейована дія застосована зі старою конфігурацією».
  2. Визначте інваріанти.
    Що завжди має бути істинним? Наприклад: «енергія пучка й режим мають збігатися з перевіреним механічним станом» відповідає «шлях запису має бути сумісним з версійною конфігурацією».
  3. Забезпечте автомати станів.
    Кожен перехід має бути явним. Ніякого «редагування на місці» для параметрів, критичних для безпеки, без версіювання та атомарного коміту.
  4. Відмовляйтесь при невизначеності.
    Якщо датчики суперечать або статус коміту невідомий — зупиніть і ескортуйте. Система має віддавати перевагу простою над неправильністю, коли можливий шкода.
  5. Зберігайте багатошарові блокування.
    Використовуйте апаратні засоби, коли можливо, але також програмні гейти, контроль доступу та runtime-ассерти. Нічого не прибирайте без заміни на сильніші докази.
  6. Проєктуйте UI для ескалації.
    Простомовні помилки, рівні серйозності та обов’язкові безпечні дії. Не дозволяйте, щоб «Enter» був шляхом до продовження небезпеки.
  7. Тестуйте в агресивних таймінгових умовах.
    Фузьте швидкість вводу, переставляйте події, інжектуйте затримки та симулюйте часткові відмови. Тести «нормального використання» доводять мало про безпеку.
  8. Незалежна верифікація та валідація.
    Розділіть зацікавленості. Команда, що випускає фічі, не має бути єдиною, хто підписує поведінку, критичну для безпеки.
  9. Інструментуйте для криміналістики.
    Захоплюйте порядок подій, знімки стану та аудиторні логи. Робіть так, щоб питання «що сталося?» відповідали протягом години.
  10. Інцидентна відповідь з правом «зупинити лінію».
    Одне достовірне повідомлення про небезпечну поведінку запускає контейнмент. Ніяких дебатів поштою, поки система продовжує працювати.
  11. Відпрацьовуйте відмови.
    Проводьте game days, з фокусом на коректності й безпеці, а не лише на доступності. Включайте операторів у вправи — вони навчать вас, де UI брешить.
  12. Відстежуйте повторювані слабкі сигнали.
    «Дивний код помилки, але допомогло після повтору» — не закритий інцидент. Це передвісник.

Чекліст релізу для змін критичних щодо безпеки або коректності

  • Чи видаляє ця зміна будь-яку валідацію, чексуми, лок або інтерлок? Якщо так: що їх замінює і які докази доводять еквівалентність?
  • Чи версійовані й атомарні переходи станів? Якщо ні — ви відправляєте умовну гонку з додатковими кроками.
  • Чи інваріанти вимірюються і гейтяться в canary/staging?
  • Чи повідомлення про помилки кажуть операторам, що робити, а не лише що сталося?
  • Чи можна реконструювати порядок подій між компонентами за 60 хвилин за допомогою логів і часових міток?
  • Чи задокументовано право «зупинити лінію» для on-call та операторів?

FAQ

1) Чи був Therac-25 «просто багом»?

Ні. Баги були, включно з таймінгозалежними, але летальний наслідок вимагав системного дизайну, який допускав небезпечні стани,
слабкі інтерлоки, погану діагностику і культуру реагування, яка не вважала ранні сигнали надзвичайними.

2) Чому умови гонки постійно з’являються в історіях про безпеку?

Тому що люди й комп’ютери створюють непередбачувані таймінги. Конкурентність породжує стани, яких ви не задумували, і тести рідко покривають
ці стани, якщо ви навмисно не атакуєте таймінг фузингом, інжекцією помилок і моделюванням автоматів станів.

3) Хіба апаратне краще за програмне?

Апаратне може бути безпечнішим для деяких гарантій, бо його складніше обійти і легше розуміти при відмові.
Але апаратура теж може ламатися. Реальна відповідь — багатошарові контролі: апаратні блокування де можливо, плюс програмні ассерти,
моніторинг і відмови в безпечний стан, коли щось не сходиться.

4) Який сучасний еквівалент Therac-25 поза медициною?

Будь-яка система, де ПЗ керує діями великої енергії чи з великим впливом: промислова автоматика, автономні транспортні засоби,
фінансовий трейдинг, ідентифікація й контроль доступу, оркестрація інфраструктури. Якщо ПЗ може непоправно нашкодити людям або порушити довіру,
ви в тому самому жанрі.

5) Чому «незрозумілі повідомлення про помилки» такі серйозні?

Бо оператори раціонально поводяться під тиском. Якщо помилка розмита й система дозволяє продовжити,
повторні спроби стають стандартом. Хороше повідомлення про безпеку — це контроль: воно диктує безпечну дію і не дозволяє випадкові обходи.

6) Як тестувати «швидкі послідовності оператора» в сучасних додатках?

Використовуйте фузинг і модельне тестування автоматів станів. Симулюйте швидкі редагування, повтори, мережеві затримки та перестановки.
У розподілених системах інжектуйте латентність і втрату пакетів; у UI скриптуйте високошвидкі інтерфейсні взаємодії і перевіряйте інваріанти на кожному кроці.

7) Як має виглядати інцидентна відповідь, коли ризик у коректності (не аптаймі)?

Трактуйте це як подію високої серйозності. Спочатку локалізуйте: зупиніть rollout, зменшіть трафік, ізолюйте підозрілі вузли та збережіть докази.
Потім перевірте інваріанти і звірте дані. Тільки після того, як ви зможете пояснити режим відмови, можна відновлювати нормальну роботу.

8) Чи сумісний «безосудовий постмортем» із відповідальністю?

Так. «Безосудовий» означає, що ви не шукаєте цапа-відбувайла за індивідуальні помилки системи. Відповідальність — це виправлення системи і виконання зобов’язань.
Якщо ваші постмортеми закінчуються «людська помилка», ви ухиляєтесь від відповідальності, а не її забезпечуєте.

9) Який один найбільш практичний урок Therac-25 для інженерів?

Не дозволяйте небезпечні стани. Закодуйте інваріанти і забезпечте їх виконання під час роботи. Якщо система не може довести, що безпечна — вона мусить зупинитися.

10) Що робити, якщо бізнес тисне на видалення «повільних» перевірок?

Тоді трактуйте перевірку як запобіжний контроль і вимагайте safety case для її видалення: що її замінює, як це буде моніторитись
і які нові режими відмов воно привносить. Якщо ніхто не може це письмово описати — відповідь «ні».

Висновок: кроки, які дійсно знижують ризик

Therac-25 — це кейс про те, як системи вбивають: не одним драматичним збоєм, а низкою малих, захищених рішень,
що прибирали тертя з небезпечних результатів. Це також кейс про те, як організації відмовляють: трактуючи ранні інциденти як шум,
довіряючи «здоровим» індикаторам більше, ніж людським повідомленням, і випускаючи системи, які не можуть пояснити себе, коли щось іде не так.

Практичні наступні кроки, у порядку впливу:

  1. Визначте й моніторьте інваріанти для коректності та безпеки, і гейтуйте релізи за ними.
  2. Моделюйте переходи станів явно і усуньте «редагування на місці» для параметрів, критичних для безпеки.
  3. Поверніть або додайте інтерлоки: апаратні де можливо, програмні ассерти всюди, і «відмовляйся при невизначеності».
  4. Покращіть спостережуваність для криміналістики: часові мітки, correlation IDs, незмінні логи і знімки стану при помилках.
  5. Запустіть таймінгово-агресивні тести: фузинг, інжекція помилок і game days, спрямовані на коректність, а не тільки на доступність.
  6. Виправте інтерфейс оператора, щоб він навчав безпечній поведінці: ясна серйозність, явні дії, без випадкових повторів через небезпеку.

Якщо ви створюєте системи, що можуть нашкодити людям — фізично, фінансово або соціально — ставте безпеку як властивість, яку потрібно постійно доводити.
День, коли ви почнете довіряти «має бути добре», — це день, коли ви починаєте писати власну главу про Therac-25.

← Попередня
Docker «bind: address already in use»: знайдіть процес і виправте акуратно
Наступна →
Heartbleed: уразливість, яка показала, що інтернет тримається на скотчі

Залишити коментар