Git submodules как пакетный менеджер: разбор недостатков и четырёх CVE
Автор статьи добавил рабочее дерево (worktree) к репозиторию, чтобы попробовать ветку рядом с основной копией, выполнил в нём git submodule update --init, потому что сборке требовались зависимости из сабмодулей, а затем попытался удалить это дерево командой git worktree remove, и git отказал. Согласно man-странице, удалять без флага --force можно только «чистые» рабочие деревья, а «нечистые или те, что содержат сабмодули» требуют --force; сабмодули вынесены в этой формулировке отдельным пунктом, не как часть «грязного» состояния. Команда git worktree move ещё строже: она отказывается работать с любым деревом, где есть сабмодули, вообще без вариантов. Это натолкнуло автора на мысль разобрать сабмодули как пакетный менеджер целиком.
Команда git worktree появилась в анонсе git 2.5 от GitHub в июле 2015 года с оговоркой: использовать её в репозитории с сабмодулями не рекомендуется. Спустя одиннадцать лет git по-прежнему требует --force для удаления такого дерева и отказывается его перемещать. Между делом команду worktree add пришлось патчить, чтобы она игнорировала настройку submodule.recurse: если её соблюдать, внутренний reset --hard начинал рекурсивно заходить в пути сабмодулей, которые в свежем дереве ещё пусты.
Центральный тезис автора: у сабмодулей уже есть все составные части пакетного менеджера, просто они плохо состыкованы. Гитлинк, запись SHA коммита по пути с режимом 160000 в дереве суперпроекта, это запись лок-файла. Файл .gitmodules, сопоставляющий пути с URL для загрузки, это манифест. Команда git submodule update, которая читает оба и заполняет рабочее дерево, это шаг установки. Точность пина не уступает любому пакетному менеджеру: это точный коммит по идентификатору объекта.
Проблема разрешения адресов: гитлинк хранит только SHA коммита, а .gitmodules несёт URL для каждого сабмодуля, и клонирование оттуда, единственный способ разрешения. Если репозиторий переименован, перенесён на другой хостинг или закрыт, все нижестоящие пины ломаются, хотя сам SHA не менялся и объекты по-прежнему есть в каждом клоне, где они уже загружены, у git просто нет способа сопоставить идентификатор коммита с сервером, который его хранит. Более того, git копирует каждый URL в .git/config суперпроекта при первом запуске git submodule init под ключом submodule.
Установка: обычный git clone записывает гитлинк в индекс, из-за чего каталог сабмодуля появляется, но остаётся пустым, пока не выполнен git submodule update --init или клонирование не сделано с флагом --recurse-submodules; настройка submodule.recurse, которая заставляет checkout, fetch, pull, grep и другие команды рекурсировать автоматически, по умолчанию выключена. По умолчанию update выкладывает закреплённый в гитлинке коммит в отсоединённом состоянии (detached HEAD); флаг --init копирует недостающие записи из .gitmodules в .git/config, а --remote вместо гитлинка выкладывает верхушку настроенной отслеживаемой ветки сабмодуля. Смена ветки в суперпроекте меняет гитлинк в индексе, но не трогает рабочее дерево сабмодуля, из-за чего git status сразу показывает его как изменённый, если только не использовать --recurse-submodules при checkout или ту же настройку submodule.recurse. Проект Rust в своём отчёте о переходе подпроектов компилятора с сабмодулей на другой механизм перечисляет ровно этот набор проблем на собственном опыте: пустые или неверно выложенные каталоги после клонирования, случайные изменения сабмодулей, попадающие в пул-реквесты из-за того, что переключение ветки оставило сабмодуль «грязным», и отдельную логику в инструменте сборки, которая перед сборкой принудительно выкладывает каждый сабмодуль на нужный коммит.
Хранение: git-каталог сабмодуля лежит под $GIT_DIR/modules/
Обновление: гитлинк хранит один SHA, поэтому продвижение сабмодуля вперёд означает зайти в него, выполнить fetch, выложить новый коммит, выйти и записать новый гитлинк командой git add
Безопасность: файл .gitmodules коммитится вместе с репозиторием, поэтому его содержимое контролирует вышестоящий репозиторий, а git разбирает этот файл при clone --recurse-submodules ещё до того, как пользователь увидел хоть один из загруженных файлов, именно эта комбинация неоднократно приводила к удалённому выполнению кода. CVE-2018-11235 использовала последовательность ../ в имени сабмодуля, из-за чего его git-каталог вместе с хуками записывался за пределы $GIT_DIR/modules/, и во время клонирования срабатывал хук post-checkout. В CVE-2018-17456 URL сабмодуля начинался с дефиса, из-за чего дочерний процесс git clone разбирал его как опцию командной строки, тот самый класс уязвимостей, от которого защищает разделитель --end-of-options. CVE-2022-39253 была уязвимостью раскрытия данных: символическая ссылка в объектном каталоге сабмодуля заставляла клонирование по локальному транспорту скопировать произвольные файлы с диска жертвы; исправление сменило значение по умолчанию для protocol.file.allow на user, из-за чего сабмодули с локальным путём теперь требуют явного разрешения. CVE-2024-32002 сочетала символическую ссылку с нечувствительной к регистру файловой системой, чтобы записать хук в каталог .git/ во время рекурсивного клонирования.
Вывод автора: сабмодули напрямую обнажают внутренности git, идентификаторы объектов как пин, отсоединённый HEAD после update, структуру каталогов $GIT_DIR/modules/, транспортные URL прямо в манифесте, тогда как пакетный менеджер оборачивает те же сущности в формат манифеста, резолвер и локальный кеш. Большинство пробелов, по мнению автора, совпадают с уже решёнными где-то ещё задачами: общий кеш объектов, рекурсия в зависимости по умолчанию при клонировании и checkout, единый жизненный цикл добавления и удаления зависимости, ограничения диапазона версий в манифесте. Апрельская серия патчей с --recurse-submodules для git worktree add закрывает ровно одну часть проблемы хранения, давая каждому рабочему дереву собственный чекаут сабмодуля поверх общего хранилища через жёсткие ссылки. Разрешение адресов автор называет более сложной, до сих пор нерешённой задачей: SHA коммита, это независимый от хоста идентификатор объекта, а URL в .gitmodules остаётся единственным известным git отображением этого идентификатора на сервер, который его хранит.
Ключевые факты
- Отправная точка: git worktree remove требует флаг --force для рабочего дерева с сабмодулями, а git worktree move с сабмодулями не работает вовсе, при том что сама команда git worktree появилась в анонсе git 2.5 в июле 2015 года с оговоркой не использовать её вместе с сабмодулями; спустя одиннадцать лет ограничение никуда не делось.
- Центральный тезис: у сабмодулей уже есть все части пакетного менеджера, гитлинк (SHA коммита в дереве) как запись лок-файла.gitmodules как манифест, git submodule update как установка, но они хуже состыкованы на каждом шаге.
- Разрешение адресов ломается структурно: URL хранится в .gitmodules и дублируется в .git/config при первой инициализации, поэтому перенос или переименование апстрима рвёт пины даже при неизменном SHA, а правка .gitmodules не подхватывается без git submodule sync.
- Общего кеша объектов нет: если два сабмодуля зависят от одного и того же репозитория, каждый получает свой каталог modules/ и свой набор объектов, в отличие от кеша cargo, хранилища pnpm или кеша модулей Go; апрельская серия патчей с --recurse-submodules для git worktree add решает похожую проблему только для рабочих деревьев, через жёсткие ссылки.
- На стыке коммита в репозиторий и автоматического парсинга .gitmodules при clone --recurse-submodules нашли четыре уязвимости: CVE-2018-11235, CVE-2018-17456 (обе, RCE), CVE-2022-39253 (раскрытие файлов через символическую ссылку) и CVE-2024-32002 (запись хука через символическую ссылку на нечувствительной к регистру ФС).
Почему это важно
Сабмодули git, одна из самых критикуемых и при этом до сих пор массово используемых частей git: разработчики регулярно внедряют их для управления зависимостями и вендоринга кода, а затем отказываются от них после серии практических проблем, автор напрямую называет «почему сабмодули git настолько плохи» повторяющейся темой обсуждений. Статья впервые системно раскладывает, где именно рвётся аналогия с полноценным пакетным менеджером: не в самой идее (гитлинк как лок-файл.gitmodules как манифест, работает), а в реализации каждого шага, разрешения адресов, установки, хранения, обновления и безопасности.
Кому это важно
Разработчикам и командам, которые уже используют сабмодули для вендоринга зависимостей или сборки монорепозитория из нескольких git-репозиториев; авторам build-инструментов и CI-пайплайнов, которым приходится обходить ограничения через url.insteadOf или писать собственную логику проверки коммитов сабмодулей (как это в итоге сделал проект Rust); специалистам по безопасности, оценивающим риск удалённого выполнения кода при клонировании репозиториев с недоверенным содержимым .gitmodules; и разработчикам самого git, которые в апреле предложили патч, решающий часть проблемы хранения для рабочих деревьев.
Как это применить
Если сабмодули уже используются: держать git обновлённым, четыре описанных CVE закрыты патчами git, а не настройками пользователя; при работе с недоверенными репозиториями явно проверять protocol.file.allow и не клонировать с --recurse-submodules вслепую; для переноса апстрима сабмодуля на зеркало или форк не забывать про git submodule sync после правки .gitmodules, иначе уже инициализированные клоны продолжат тянуть старый URL; в CI переписывать адреса через url.
Можно ли доверять
Материал написан в жанре подробного технического разбора: автор цитирует конкретные команды, вывод man-страницы, формулировку оригинального анонса git 2.5, ветку рассылки git (вопрос Ксавье Мореля) и серию патчей, а также приводит номера четырёх CVE с описанием механизма каждой уязвимости и итоговым исправлением. Это стиль, подкреплённый проверяемыми первоисточниками (документация git, список рассылки, база CVE), а не пересказ чужого мнения, оценка достоверности источника (82) отражает именно это. Как и в любом авторском разборе, общая рамка «сабмодули как пакетный менеджер», точка зрения автора, но фактические детали (версии, номера CVE, поведение команд) сформулированы как проверяемые технические утверждения, а не догадки.
Риски и подводные камни
Главный риск, сочетание доверия к содержимому репозитория и автоматизма: .gitmodules коммитится вместе с кодом и разбирается git ещё до того, как пользователь видит загруженные файлы, и именно эта комбинация уже как минимум четырежды приводила к RCE или раскрытию файлов (CVE-2018-11235, CVE-2018-17456, CVE-2022-39253, CVE-2024-32002). Второй риск, тихая рассинхронизация состояния: смена ветки в суперпроекте не обновляет рабочее дерево сабмодуля автоматически, из-за чего git status сразу показывает изменения, а случайные откаты сабмодулей могут попасть в пул-реквест незамеченными, ровно это описывает опыт проекта Rust. Третий, отсутствие диапазонов версий: единственный плавающий ориентир в манифесте это имя ветки, а единственный пин, гитлинк, поэтому автоматическое обновление через Dependabot или Renovate работает только по принципу «подтянуть верхушку ветки», без промежуточных ограничений.
«Использовать git worktree в репозитории, где есть сабмодули, не рекомендуется.»
— анонс git 2.5, GitHub, июль 2015