Germ: разработчик GNU Mes показал Scheme-интерпретатор на 2,25 КБ для сборки GNU Guix с нуля

В 2026 году ежегодная конференция свободного и открытого ПО FOSSY (Free and Open Source Software Yearly) впервые за три предыдущих издания сменила площадку: вместо Портленда, Орегон, она прошла в кампусе университета Британской Колумбии (UBC) в Ванкувере. В треке про инструменты и тулчейны разработчик Тимоти Сэмпл выступил с докладом о сборках с нуля (в оригинале, bootstrappable builds): при таком подходе крошечная программа собирает чуть более сложную, та, ещё более сложную, и так далее, пока из минимального «зерна» не соберётся вся пользовательская часть современной Linux-системы. В отличие от воспроизводимых сборок (reproducible builds), которые лишь подтверждают, что готовый бинарник совпадает с исходным кодом, сборка с нуля отвечает на другой вопрос, чем изначально был собран сам инструмент сборки.

Это прямой ответ на угрозу из лекции Кена Томпсона «Reflections on Trusting Trust» («Размышления о доверии доверию»): бэкдор можно спрятать не в исходном коде, а в бинарнике самого компилятора, и тогда чтение исходников ничего не покажет, вредоносная вставка будет автоматически переноситься в каждый следующий собранный этим компилятором компилятор. Сэмпл сослался на статью с разбором реальной атаки такого рода на дистрибутив NixOS: исследователи внедрили бэкдор в утилиту strip, которая запускается почти на каждом собранном в системе бинарнике, и получили возможность подменять практически любую программу так, что анализ исходного кода это не показывал. Воспроизводимые сборки эту брешь не закрывают: если пересобранный бинарник не совпал с оригиналом, значит, кто-то солгал или ошибся насчёт его происхождения, а не то, что в сам инструмент сборки изначально подложен бэкдор.

Дальше Сэмпл рассказал историю цепочки сборки Guix, функционального пакетного менеджера в духе Nix, где для каждой программы в общем графе расписано, из чего и как её собрать, вплоть до собственного компилятора. Для дистрибутива вроде Debian эта цепочка обрывается на бинарнике C-компилятора, когда-то просто загруженном кем-то в репозиторий, происхождение такого бинарника, по сути, неизвестно. У Guix стартовой точкой раньше был заранее собранный статический бинарник программ GNU размером 250 мегабайт; сегодня всё начинается с 256-байтной программы hex0, которая переводит строку шестнадцатеричного текста в биты бинарника и затем слой за слоем доводит систему до GCC 2, GCC 4, современного GCC и современного Guile. Есть и оговорка: даже в этой цепочке ещё используется статически слинкованный бинарник Guile (в проекте его называют %bootstrap-guile), Сэмпл сам назвал это «абсолютно нечестным приёмом» и сказал, что работает над тем, чтобы это исправить. Полная низкоуровневая цепочка идёт через hex1 и hex2 (те же конвертеры, но с метками и адресацией), M0 (уже с ассемблерными мнемониками вместо шестнадцатеричных кодов) и M2-Planet (урезанный диалект C), а на этом этапе всё переключается на GNU Mes: Scheme-интерпретатор, написанный на диалекте M2-Planet, с собственной библиотекой Meslibc и Scheme-компилятором MesCC, который умеет собрать Tiny C Compiler (TCC); дальше TCC уже собирает современные инструменты разработки. Родственный проект live-bootstrap идёт тем же путём, но дальше, он ещё пытается довести до состояния «с нуля» и ядро (использует ядро Fiwix) и заново генерирует служебные файлы вроде configure; весь его путь до базовой Linux-системы, без Rust и Go, Сэмпл показал на слайде как список из 182 шагов. Отдельно он привёл в пример Rust: сегодняшняя цепочка Guix стартует с C++-инструмента mrustc, который собирает Rust версии 1.54 или 1.56, а дальше приходится последовательно пересобрать почти каждую версию вплоть до нынешней версии Rust 1.97, один слушатель рассказал, что такая сборка заняла у него три дня на ноутбуке с процессором Arm; сам mrustc уже умеет собирать Rust 1.90, но в цепочку Guix это ещё не встроено.

Основную часть доклада Сэмпл посвятил собственному проекту Germ (он же «Germ Lisp»), попытке срезать путь от hex0 до Mes. По его наблюдению, нынешняя цепочка идёт от C к Scheme и обратно к C, чтобы в итоге прийти к Scheme-based Guix, хотя «примитивный Lisp-интерпретатор не намного сложнее примитивного ассемблера». Germ весит около 2,25 КБ (Сэмпл целился в 2 КБ, но не уложился) и умеет исполнять «почти-Scheme», этого достаточно, чтобы запустить написанный на Scheme ассемблер, который в свою очередь строит вторую, уже более полную стадию интерпретатора с байтовыми векторами, вводом-выводом и работой с ядром; она умеет собирать C-код через MesCC, а ещё запускать скрипты командной оболочки через написанную Сэмплом реализацию такой оболочки на Scheme (в перспективе, и скрипты на awk и sed). Сам доклад был одновременно демонстрацией: слайды всё это время показывались через интерфейс к SDL, написанный поверх Germ, а шрифт для них Сэмпл выбрал сам, зал угадал в нём шрифт Lisp-машины Symbolics.

Главная проблема Germ, производительность. На микробенчмарке вызовов функций он формально быстрее Mes, но на реальном коде проигрывает: почти всё в Germ выполняется в самом Scheme, включая циклы. Сборка Mes самим собой занимает около 100 секунд (60, с экспериментальным байт-код-компилятором Mes), а через Germ, даже с включёнными тестовыми оптимизациями, 140, 150 секунд; сам Mes Сэмпл при этом называет «невыносимо медленным», так что быть медленнее него, почти провал задачи. Портируется Germ тоже хуже: Mes работает на Arm и RISC-V, а Germ пока только на x86_64, порт на RISC-V начат, но не закончен. Отвечая на вопрос из зала, Сэмпл уточнил, что Germ, это, по сути, диалект Guile Scheme без надстройки GOOPS (объектной системы Guile), фактически совместимый с R7RS, но без чисел с плавающей запятой; систему макросов syntax-case ему пришлось реализовать самому, потому что готового bootstrappable-варианта такого макрос-движка попросту не существует.

В дискуссии после доклада Кит Паккард заметил, что писать на ассемблере проще, чем на Forth, ещё одном языке, на котором иногда пытаются бутстрапить системы. Марк Вилард спросил, сколько из 182 шагов live-bootstrap уже снимает Germ; Сэмпл честно ответил, что пока это скорее мечта, он надеется, что Germ станет «клином», который поможет и другим разработчикам сокращать длинные цепочки вроде тех, что нужны для Perl и Autoconf. Он согласился с прикидкой Виларда, что даже в лучшем случае в цепочке останется больше 80 шагов, а Вилард тут же привёл пример: незаметный саботаж, скажем, шага 73, скорее всего остался бы необнаруженным. Сэмпл признал это известным изъяном подхода и сказал, что цель, постепенно сокращать число шагов, а не решить проблему верификации разом. На вопрос, можно ли сделать зерно ещё компактнее 2,25 КБ, Сэмпл согласился, что можно, например, стартовав прямо с hex0, но объяснил, что не хочет играть в игры, а хочет реально получить результат, и что использование hex0 для этой цели не осуждает, но для себя считает немного нечестным приёмом. Закрыл доклад Сэмпл демонстрацией REPL самого Germ, тем же, через который читал слайды: показал вывод трассировки при ошибках и заметил, что без таких «удобств» работать в интерпретаторе каждый день было бы невыносимо.

Ключевые факты

  • На конференции FOSSY 2026 в Ванкувере Тимоти Сэмпл, один из тех, кто пишет и поддерживает GNU Mes, показал Germ: Scheme-интерпретатор весом 2,25 КБ (целился в 2 КБ), который предлагает как новую отправную точку для сборки Guix с нуля.
  • Сборка с нуля (bootstrappable build) защищает от угрозы из лекции Кена Томпсона «Reflections on Trusting Trust», бэкдора, спрятанного в бинарнике компилятора и невидимого в исходниках; Сэмпл сослался на реальный случай такой атаки на NixOS через утилиту strip, которая запускается почти на каждом бинарнике системы.
  • Стандартная цепочка Guix начинается с 256-байтной программы hex0 (раньше, с 250-мегабайтного заранее собранного бинарника) и проходит через hex1/hex2, M0, M2-Planet, GNU Mes и Tiny C Compiler; у родственного проекта live-bootstrap весь путь до базовой Linux-системы, 182 шага, и, по оценке слушателя Марка Виларда, даже после дальнейших сокращений их останется больше 80.
  • На реальном коде Germ пока медленнее Mes: сборка Mes самим собой занимает 100 секунд (60, с экспериментальным байт-код-компилятором), а через Germ, 140, 150 секунд; портируется Germ тоже хуже, работает только на x86_64, тогда как Mes, ещё и на Arm и RISC-V.
  • На вопрос, сколько из 182 шагов Germ уже убирает, Сэмпл честно ответил: пока это скорее мечта, лишь надежда стать «клином» для сокращения таких цепочек в будущем; Марк Вилард в ответ заметил, что саботаж, например, шага 73, скорее всего остался бы незамеченным, и Сэмпл назвал это известным изъяном подхода.

Почему это важно

Сборка с нуля закрывает конкретную и хорошо задокументированную угрозу, атаку типа «Reflections on Trusting Trust» Кена Томпсона, когда бэкдор живёт не в исходном коде, а в бинарнике компилятора, и потому невидим при чтении исходников и автоматически переживает каждую следующую пересборку. Это не гипотетика: Сэмпл привёл в пример реальную атаку на NixOS, где бэкдор в утилите strip, она запускается почти на каждом собранном в системе бинарнике, позволил незаметно подменять практически любую программу, и анализ исходного кода этого не показывал. Обычные воспроизводимые сборки (reproducible builds) от такой атаки не защищают: они лишь подтверждают, что бинарник совпадает с исходниками, но не то, что сам инструмент сборки изначально чист.

Кому это важно

В первую очередь, мейнтейнерам дистрибутивов и пакетных менеджеров с претензией на полную проверяемость происхождения кода (Guix, Nix и похожие проекты), а также инженерам, которые занимаются безопасностью цепочки поставки ПО (supply chain security) и всерьёз спрашивают, чем был собран компилятор, собравший компилятор. Рядовому прикладному разработчику или пользователю Linux, который не пересобирает свой тулчейн с нуля, тема пока сугубо теоретическая: и полная цепочка Guix/live-bootstrap, и новый инструмент Сэмпла, это нишевый инженерный проект сообщества, а не готовый продукт или практика, которую стоит немедленно перенимать.

Как это применить

Проактивный подход к бутстрапу, по словам Сэмпла, лучше всего работает на опережение: например, GNU Guile (диалект Scheme, на котором построены сами скрипты сборки Guix) держит и поддерживает старую реализацию на C именно для того, чтобы было чем бутстрапить компилятор, а GNU Make, помимо основного make-файла, хранит запасной shell-скрипт на случай, если make ещё не собран. Там, где у инструмента есть только самостоятельная (self-hosted) сборка, сообщество bootstrappable-builds использует два приёма: «археологические раскопки», найти в истории проекта версию без самостоятельной сборки и затем шаг за шагом пересобирать все версии до современной (для Rust это, например, десятки версий подряд), либо написать отдельный компактный инструмент специально для запуска первой сборки, как mrustc для Rust. Лучшие результаты даёт их комбинация: сначала бесхитростный инструмент поднимает старую версию, а дальше её пересобирают вперёд, версия за версией, до актуальной. Germ встраивается в эту же логику как альтернатива нижним звеньям цепочки Guix, в перспективе именно он (или, как вариант, Mes) мог бы заменить собой статически слинкованный бинарник Guile (%bootstrap-guile), который Guix сейчас использует как признанный самим проектом «нечестный» шорткат; но это ещё не готовая замена, а рабочий эксперимент самого Сэмпла, показанный вживую, его собственные слайды на докладе рисовались через SDL-надстройку над Germ, и сроков интеграции в реальный Guix пока нет.

Можно ли доверять

Это репортаж LWN.net с одного доклада на профильной конференции (FOSSY 2026, трек Toolchains and Other Development Tools), издание давно и подробно освещает именно такую техническую кухню open source, а сам докладчик не случайный человек, а один из тех, кто пишет и поддерживает GNU Mes. При этом почти все цифры в статье, это то, что Сэмпл сообщил со сцены (например, тайминги компиляции на его собственном настольном компьютере), а не результат независимого замера или рецензируемого исследования, и сравнение Germ с Mes сделано на одной машине докладчика. Сам Сэмпл не приукрашивает результат: открыто говорит, что Germ пока x86_64-only, на реальных программах медленнее Mes, чуть превысил целевой размер (2,25 КБ вместо 2 КБ) и лишён части Scheme (например, чисел с плавающей запятой), а на прямой вопрос, сколько шагов бутстрапа Germ уже убрал, ответил честно: пока это скорее мечта, надежда на будущее, а не готовый результат.

Риски и подводные камни

Главный риск, производительность: хотя на микробенчмарке вызовов функций Germ формально быстрее Mes, на реальной сборке он медленнее (140, 150 секунд против 100, или 60 с экспериментальным байт-код-компилятором Mes), а сам Mes и так был признан Сэмплом «невыносимо медленным», то есть Germ пока проигрывает планку, которая и без того была низкой. Портируемость тоже хуже: Mes работает на Arm и RISC-V, Germ, пока только на x86_64. Системная проблема остаётся нерешённой даже в теории: по оценке Марка Виларда, даже после всех возможных сокращений в цепочке бутстрапа останется больше 80 шагов, а саботаж, скажем, шага 73, по его словам, скорее всего прошёл бы незамеченным, Сэмпл прямо назвал это известным изъяном подхода. Наконец, есть и человеческий барьер: Scheme и Lisp вызывают стойкое отторжение у части разработчиков («люди просто ненавидят скобки», как выразился сам Сэмпл), и это тоже ограничивает шансы Germ стать чем-то большим, чем личный проект энтузиаста.

«Они смогли встроить бэкдор практически в каждую программу системы, так, что это невозможно было обнаружить анализом исходного кода.»

— Тимоти Сэмпл, доклад на FOSSY 2026