EnglishРусский Map

Бомба замедленного действия в TCP macOS: переполнение ядра через 49 дней

title
Бомба замедленного действия в TCP macOS: переполнение ядра через 49 дней
type
summary
summary
32-битный tcp_now в ядре XNU переполняется через 49,7 дня, замораживая TIME_WAIT и полностью ломая TCP
tags
networking, macos, kernel, bugs
created
2026-04-07
updated
2026-07-29
lang
ru
translation_of
macos-tcp-time-bomb
source_updated
2026-07-29
translated
2026-09-01
translator
lllm/antigravity/gemini-3.7-flash-medium

Команда Photon обнаружила, что у каждого Mac есть скрытый срок годности. Ровно через 49 дней, 17 часов, 2 минуты и 47 секунд непрерывного аптайма переполнение 32-битного беззнакового целого в ядре XNU замораживает внутренние часы временных меток TCP. Соединения в состоянии TIME_WAIT перестают завершаться, эфемерные порты исчерпываются, и в итоге любые новые TCP-соединения перестают работать. Ping при этом продолжает отвечать. Единственное решение - перезагрузка.

Переполнение

Ядро XNU отслеживает время TCP с помощью tcp_now - переменной типа uint32_t, которая инкрементируется с миллисекундной точностью в calculate_tcp_clock():

current_tcp_now = (uint32_t)now.tv_sec * 1000 + now.tv_usec / TCP_RETRANSHZ_TO_USEC;

uint32_t tmp = os_atomic_load(&tcp_now, relaxed);
if (tmp < current_tcp_now) {
    os_atomic_cmpxchg(&tcp_now, tmp, current_tcp_now, relaxed);
}

2^32 миллисекунд = 49,7 дня. Когда результат умножения превышает максимум uint32_t, приведение типа сбрасывает current_tcp_now обратно к околонулевым значениям. Проверка монотонности (if (tmp < current_tcp_now)) сравнивает старое значение (~4,29 миллиарда) с новым сброшенным значением (~5 000). Условие оказывается ложным. Инструкция cmpxchg больше не срабатывает. tcp_now намертво застывает на своём последнем значении до переполнения и больше никогда не увеличивается.

Почему ломается TIME_WAIT

Когда TCP-соединение переходит в состояние TIME_WAIT, ядро записывает tcp_now + 30000 (2 × MSL, 30 секунд в macOS) в качестве временной метки истечения срока жизни. Сборщик мусора проверяет TSTMP_GEQ(tcp_now, timer), где используется знаковая модульная арифметика: ((int)((a)-(b)) >= 0).

Поскольку tcp_now заморожен, эта проверка всегда возвращает false для любого соединения, созданного после переполнения: его таймер перевалил за максимум uint32_t и стал небольшим числом, а frozen_value - small_number дает большое отрицательное число. В итоге соединение так и не очищается.

Каскадный сбой

Сбой происходит незаметно и развивается постепенно:

  1. Минуты: соединения в TIME_WAIT перестают закрываться. На машинах с низким трафиком это может остаться незамеченным.
  2. Часы: количество соединений в TIME_WAIT накапливается до тысяч. Начинают заканчиваться эфемерные порты (49152-65535, около 16K в macOS).
  3. Исчерпание портов: новые исходящие соединения не могут занять порт. Они зависают в состоянии SYN_SENT и завершаются ошибкой. Уже существующие соединения в состоянии ESTABLISHED продолжают работать.
  4. Всплеск нагрузки на систему: ядро тратит процессорное время на перебор постоянно растущей очереди TIME_WAIT. На одной из тестовых машин load average достиг 49.74.
  5. TCP полностью умирает: работает только ICMP, поскольку он не использует порты TCP и подсистему таймеров TCP.

Никаких kernel panic, записей в логах или отчётов о падении. Система выглядит полностью исправной, пока TCP просто не перестаёт работать.

Воспроизведение на практике

Команда Photon провела контролируемый эксперимент на двух компьютерах Mac из своего парка мониторинга iMessage, которые приближались к отметке в 49,7 дня. Они генерировали ~15 коротких TCP-соединений каждые 2 секунды до и после момента переполнения.

До переполнения: число соединений в TIME_WAIT находилось в динамическом равновесии на уровне ~200 (7,5 соединений/с × 30 с времени жизни). Создание и освобождение балансировали друг друга.

После переполнения: число соединений в TIME_WAIT монотонно росло. На машине B оно достигло 2 828 на момент остановки скрипта и 8 217 через девять часов. Ни одно соединение так и не было закрыто. Показатель load average на машине B подскочил до 49.74 с 3 315 соединениями в состоянии SYN_SENT, зависшими на первом шаге рукопожатия.

Семейство целочисленных переполнений

Этот баг относится к хорошо известному классу:

  • Windows 95/98 - 32-битный счётчик миллисекундных тиков переполнялся через 49,7 дня, что приводило к зависанию системы
  • Y2K38 - знаковый 32-битный time_t в Unix переполнится 19 января 2038 года
  • GPS Week Number Rollover - 10-битный счётчик переполняется каждые 19,7 года
  • Экран смерти в Pac-Man - 8-битный счётчик уровней переполняется на числе 256

Паттерн один и тот же: код предполагает, что счётчик только растёт, и не обрабатывает переход через границу. Всё работает годами. А затем ломается на строго определённой математической границе.

Кто затронут

Любой Mac с аптаймом больше 49,7 дня и любым TCP-трафиком. Большинство пользовательских Mac перезагружаются чаще из-за обновлений системы. В зоне высокого риска:

  • Долгоживущие серверные парки (Mac mini, Mac Pro)
  • Серверы сборок CI/CD (Jenkins, self-hosted runner'ы GitHub Actions)
  • Размещённые в дата-центрах (colocated) компьютеры Mac с удалённым управлением
  • Рабочие станции, выполняющие длительный рендеринг или симуляции

Сообщения сообщества

Симптомы из множества предыдущих сообщений совпадают один в один - TCP отказывает, ICMP работает, перезагрузка помогает, проблема проявляется после нескольких недель аптайма - но ни в одном из них не была выявлена первопричина:

  • Темы на Apple Community #250867747 и #252991075
  • Issue в репозитории Podman #12495 (виртуальная машина с macOS 12 теряет TCP после длительного аптайма)

В статье баг отслеживается до конкретной строки в bsd/netinet/tcp_var.h и bsd/netinet/tcp_timer.c ядра XNU с полной причинно-следственной цепочкой от microuptime() через calculate_tcp_clock() до tcp_gc().

Связанные материалы: tcp-congestion-control разбирает внутреннее устройство TCP. Механизм TIME_WAIT и значения MSL, о которых идёт речь, входят в тот же конечный автомат TCP, который 13kb-tcp-slow-start рассматривает со стороны управления перегрузкой.

macos-executable-replacement - находка аналогичного формата в macOS, но с противоположной реакцией вендора: Apple воспроизвела отчёт Mysk о том, как код с правами обычного пользователя подменяет бинарник внутри скачанного бандла приложения, и закрыла обращение как не требующее исправления. Переполнение через 49 дней - это однозначный баг; в том же случае возникли разногласия о том, где именно проходит граница безопасности.