Утром 24 сентября 2026 года компания Endcrawl, чей сервис готовит списки актёров и титры для голливудских фильмов, на полдня осталась без продакшн-сервера. Сооснователь компании Алан Гроу хотел всего лишь отменить тестовый рендер и набрал классическую команду: sudo kill -TERM -1490947. Минус перед числом означал не «убей процесс с этим номером», а «убей всю группу процессов» — приём старый, как job control. Вместо группы рендеров погасло всё: базы, веб-серверы, мониторинг.
Пережил расстрел один лишь init — процесс с номером 1, у которого по старому unix-правилу иммунитет. Гроу первым делом обвинил себя: опечатка в команде или сбойный скрипт. Ошибся он в другом направлении — баг сидел в самой утилите kill, ровеснице Unix, которую весь мир дёргает миллиарды раз в день. А копнув глубже, он нашёл целую серию багов, жившую в Ubuntu LTS больше десяти лет.
Зацепило с дайджеста Lobsters: заголовок «A Decade of Kill Bugs», одна команда в первой строке и обещание, что до корня там копать далеко. Дайджест дал только событие — сервер умер. Суть осталась за кадром: почему утилита полувекового возраста с простейшей спецификацией вообще может положить всю машину от одного неудачного дефиса.
Раскопки стоили того по трём причинам. Первая — красивая механика: внутри провинившейся библиотеки getopt отрицательный PID схлопывается в один символ, и гигантский номер группы превращается в «минус один» — ядерный код, означающий «вся машина». Вторая — история: это уже четвёртый по счёту такой баг, и все они разъехались по LTS-релизам Ubuntu, той ветке, которую держат на серверах именно потому, что она «не меняется». Третья — археология: само право убивать процессы минусом родилось в 1985-м благодаря Sun, и с тех времён парсер командной строки так и не научился однозначно читать строку, где дефис означает и сигнал, и группу процессов.
Разбор вскрыл неожиданное: никакого переполнения чисел, никакой экзотики с гигантским PID. Код /bin/kill — утилиты из пакета procps, стандартного набора системных инструментов Linux, — читал переменную optopt: стандартную «переменную ошибки» C-функции getopt, разбирающей аргументы командной строки. getopt устроен строго: когда он встречает опцию, которой нет в списке, он возвращает код «неизвестная опция» и кладёт в optopt первый символ после дефиса. Не весь аргумент — только первый байт. Из строки -1490947 функция извлекла символ 1, а дальше сработала строка кода, вычислявшая PID как разность кодов символов: '0' - '1'. В кодировке ASCII это минус один.
Дальше сцену подхватывает ядро. В man-странице системного вызова kill(2) про минус один написано прямо: сигнал доставляется каждому процессу, которому вызывающий имеет право его послать, кроме процесса номер один. Единственный, кому разрешено выжить, — init, первый процесс системы. Так строковое чутьё разработчиков разделило машину на «всех, кого можно убить» и одного неприкосновенного. Команда выполнялась через sudo, а sudo работает от root — root имеет право послать сигнал всему.
Второй ярус находки — баг не был одиноким. Гроу прогнал эксперимент над парсерами kill из разных LTS-релизов Ubuntu и получил коллекцию, достойную музея bad code:
| Ubuntu | procps | Что делал парсер с отрицательным PID | Чем аукнулось |
|---|---|---|---|
| 14.04 | 3.3.9 | Переход на getopt: kill -HUP -123 склеивался в буквы -1 -2 -3 |
-12345 работал, -1 молча ничего не делал |
| 16.04 | 3.3.10 | Костыль pid = '0' - optopt |
-12345 превращался в -1: убить всё |
| 18.04 | 3.3.12 | atoi(argv[optind]) при уже съеденном аргументе |
segfault самой kill |
| 22.04 | 3.3.17 | atoi(argv[optind-1]) при НЕ съеденном аргументе |
kill -TERM -123 убивал процессы родительской shell |
| 24.04 | 4.0.4 | Воскрешение костыля из 16.04 | Сценарий Endcrawl |
Механика первого бага — отдельный гиньоль. getopt поддерживает «склейку» коротких опций: tar -xvf это просто -x -v -f. Для сигналов -HUP это тоже склейка — -H -U -P. Аргумент потребляется индексом optind только на последнем символе связки, а код procps выскакивал из цикла getopt на первой же ошибке «?» — до того, как индекс доехал до конца.
Итог выглядел как шизофренический: длинный PID -12345 обрабатывался правильно (аргумент оставался нетронутым и попадал в strtol-парсер), а короткий -1 — «ничего не происходило», потому что getopt уже съел его как опцию. Починили как раз короткий случай — трюком с optopt — и ценой этого стали гигантские номера групп, превращающиеся в минус один.
Штука в том, что каждый следующий релиз «чинил» предыдущий способ — и ронял новый. Пять версий, пять разных способов выстрелить в ногу, и всеми ими можно было пользоваться годами. Ответственность за регресс 22.04 к тому же легла на родительскую shell вместо kill'а, из-за чего фейл выглядел как «упала моя сессия по непонятной причине».
Аутейдж Endcrawl — не первый случай. Баг-репорт #1637026 в Launchpad открыт аж в 27 октября 2016-го: dann frazier — известный kernel-разработчик — зафиксировал, что kill валит проды при деплое Hadoop с формулировкой «basically killing everything on the system». То есть серию убитых машин задокументировали, пофиксили в 16.04 ... и код с тем же грехом в 2026-м тихо воскрес в 24.04.
А трассировка dann frazier с kill(4294967295, SIGUSR2) выглядит как шахматный этюд: strace печатает аргумент как unsigned, а на самом деле это то самое минус один, «убей всех».
Мораль двойная. Первая: багтрекер — это оперативная память индустрии, не долговременная; fix Released в одном релизе совсем не гарантирует, что идея кода не вернётся в другом. Вторая: баги такого класса живут десятилетиями, потому что их триггер настолько редок, что между двумя попаданиями проходят годы — и каждый раз кажется, что «такое бывает только у других».
Само право на «убей минусом» — красивый детектив. В 1985-м Sun предоставила код ядра BSD для отрицательных PID (в man-странице — «для совместимости с System V»), в 1986-м это добралось до userland-kill'а и одновременно появилось в Unix V9. Откуда именно пришла идея — из System V или из SunOS — выяснить до сих пор не удалось: архивов System V восьмидесятых в открытом доступе нет, что для unix-историка сродни потерянной сцене из немого кино. Tony Finch (fanf с Lobsters) раскопал это в CSRG-архивах прямо в треде под статьёй.
Отдельная глава — Linux-родословная. В девяностые userland переписывали с чистого листа из юридических соображений (BSD-код), и kill достался Albert Cahalan — который, по легенде с LWN, сам себя однажды DoS'нул пятью fork'ами при тестировании. Поддержку потом подхватил Debian-мейнтейнер Craig Small — и тянет её до сих пор.
Практический минимум, который стоит увезти:
--. kill -TERM -- -1490947 корректно парсится даже на всех пяти битых версиях — getopt перестаёт считать аргумент опцией. GNU-подкованный рефлекс «после дефиса — always --» здесь спасает буквально всё.sudo -u render kill ... при том же промахе убил бы только render-процессы, а не всю машину. Root — это право послать сигнал всему, включая себя случайно.-s/--signal — а вот для process group за сорок лет явного флага так и не появилось. Двуслойная интерпретация дефиса — налог, который система собирает раз в десятилетие.Гроу закрывает статью строчкой, которую хочется повесить над дежуркой: «An outage not understood is an outage that happens again». Он же в треде выдал заготовку названия для будущей лекции: «Getopt Considered Harmful. Well okay, at least in this specific case».