Алексей Меленчук
вся записка
дата
объём 4 мин чтения

Воспроизводимая сборка: от Томпсона до SOURCE_DATE_EPOCH

Кен Томпсон в 1984 году показал, что исходный код не доказывает свойств бинарника. Что из этого следует и какие источники недетерминизма приходится закрывать.

Коротко. Побайтово воспроизводимая сборка — не про эстетику, а про то, что бинарник можно связать с исходником проверкой, а не доверием. Мешают этому не заговоры, а метки времени, порядок файлов и пути сборки.

Кен Томпсон получил премию Тьюринга в 1983 году, а лекцию прочёл о том, как он встроил бы бэкдор в систему так, чтобы его нельзя было найти чтением исходного кода. Текст лекции — «Reflections on Trusting Trust», опубликован в CACM в 1984 году.

Схема двухступенчатая. Компилятор изменяется так, чтобы при сборке программы входа в систему добавлять бэкдор. Это видно в исходном коде компилятора. Тогда компилятор изменяется второй раз: он распознаёт сборку самого себя и вставляет в получившийся бинарник обе закладки. После этого правку из исходного кода компилятора убирают.

Исходный код компилятора чист. Бинарник компилятора воспроизводит закладку и в программе входа, и в следующей версии самого себя.

Вывод Томпсона: исходный код не доказывает свойств бинарника, если бинарник собран инструментом, который вы не проверяли.

Почему это перестало быть теоретическим

Тридцать лет это цитировали как красивый парадокс. Потом атаки на цепочку поставки перестали быть гипотезой — заражение среды сборки и подмена артефактов стали регулярной практикой.

И тогда стал важен вопрос: как вообще проверить, что вот этот бинарник собран вот из этого исходника?

Ответ у проекта Reproducible Builds простой по формулировке: сборка из одного исходника должна давать побайтово одинаковый результат у кого угодно и когда угодно. Тогда несколько независимых сборщиков собирают одно и то же, сравнивают хеши, и подмена в одной среде становится видна.

Это не защищает от Томпсона напрямую — если все используют один заражённый компилятор, результат совпадёт у всех. Против самого Томпсона работает разнообразная двойная компиляция Дэвида Уилера: исходник компилятора собирается вторым, независимым компилятором, и результатом пересобирается сам себя; при отсутствии закладки оба пути сходятся к одному бинарнику. Приём описан и обоснован формально.

Для обычного проекта воспроизводимость решает более скромную, но живую задачу: связать артефакт с исходником проверкой, а не доверием к тому, у кого была консоль.

Что ломает воспроизводимость на практике

Ни одного заговора. Обычный недетерминизм.

Метки времени. Дата сборки, зашитая в бинарник, в архив, в манифест. Каждая сборка отличается. Для этого проект Reproducible Builds ввёл переменную SOURCE_DATE_EPOCH: инструменты, которые её понимают, подставляют её вместо текущего времени.

Порядок обхода файловой системы. readdir не гарантирует порядок. Список файлов, собранный обходом каталога, у двух машин может отличаться, и вместе с ним отличается порядок объектных файлов в архиве и порядок символов в таблице. Лечится явной сортировкой.

Пути сборки. Абсолютный путь попадает в отладочную информацию и в сообщения об ошибках. Сборка в /home/alex/project и в /build даёт разные бинарники. Лечится переназначением префикса (-ffile-prefix-map и его аналоги).

Параллельность. Порядок завершения задач влияет на порядок записи в общий выход. Классическая ловушка: конкатенация результатов в порядке готовности, а не в порядке объявления.

Порядок обхода хеш-таблиц. Языки со случайным зерном хеширования дают разный порядок итерации между запусками. Если этот порядок влияет на вывод — генерация кода, сериализация — результат плавает.

Локаль и часовой пояс. Сортировка строк зависит от локали, форматы дат — от пояса.

Метаданные архивов. tar и zip пишут владельца, права и время изменения. Два одинаковых по содержимому архива отличаются побайтово.

С чего начать, не переписывая всё

Полная воспроизводимость — большая работа, особенно на языках с богатой экосистемой. Но первые шаги дают непропорционально много.

  1. Зафиксировать зависимости по хешу, а не по версии. package-lock, poetry.lock, go.sum, Cargo.lock. Диапазон версий означает, что вчерашняя и сегодняшняя сборка — разные программы.
  2. Собирать в контейнере с закреплённым образом по дайджесту, а не по тегу. Тег node:22-slim указывает на разные образы в разные дни.
  3. Выставить SOURCE_DATE_EPOCH в дату последнего коммита.
  4. Сравнить два прогона. Собрать дважды и сравнить хеши. Расхождение покажет, где именно течёт недетерминизм, и обычно это одно-два места, а не двадцать.

Четвёртый шаг стоит десяти минут и отвечает на вопрос, который иначе остаётся открытым годами.

Чего это не даёт

Воспроизводимость не делает код безопасным. Она делает проверяемой связь между исходником и артефактом — и только.

Уязвимость в зависимости останется уязвимостью, воспроизводимо собранной. Ценность в другом: когда артефакт оказывается заражённым, воспроизводимая сборка позволяет ответить, был ли исходник тем же. Без неё этот вопрос не имеет ответа, и расследование упирается в «наверное».

Когда этим можно не заниматься. Внутренний инструмент, который собирается на одной машине и там же работает, ничего от воспроизводимости не получает. Разговор начинается там, где артефакт уезжает к кому-то ещё — в реестр образов, в поставку, в чужую инфраструктуру.