SoLo даёт статическим Linux-бинарникам системные GPU-драйверы без контейнера

На GitHub, в репозитории pg83/solo, опубликован проект SoLo, библиотека, которая даёт полностью статически слинкованному Linux-бинарнику на musl доступ к системным разделяемым библиотекам (.so), собранным под glibc, включая GPU-драйверы Vulkan и OpenGL. Обычно статический musl-бинарник не может вызвать dlopen() для такой библиотеки: у него просто нет второй libc в процессе. SoLo решает это не подключением второй libc и не запуском системного динамического загрузчика, а собственной реализацией: у неё есть свой ELF-загрузчик (lib/elf_loader.cpp) для x86-64 и aarch64, который сам маппит сегменты ELF, обходит DT_NEEDED, резолвит версионированные символы, применяет релокации, поддерживает TLS и TLSDESC, материализует IFUNC-и, применяет RELRO и запускает инициализаторы. Поверх этого лежит glibc_shim.cpp, слой, который транслирует импорты вида malloc@GLIBC_2.2.5 в ABI-совместимые обёртки над уже работающим в процессе рантаймом musl; для функций glibc, которые SoLo не поддерживает, сгенерированы явные заглушки, которые при вызове громко падают с именем и версией символа, вместо того чтобы тихо портить память процесса.
Мост описан как нечто большее, чем простая обёртка над dlsym: он умеет пробрасывать C++-исключения в обе стороны между musl- и glibc-кодом (unwind проходит через кадры обеих сторон, деструкторы отрабатывают на каждой), поддерживает все четыре модели TLS без патчинга кода, воспроизводит семантику связывания ld.so, глобальную область видимости символов, RTLD_DEEPBIND, DT_SYMBOLIC, версионирование символов, ленивое связывание PLT, поиск по GNU- и SysV-хэшам, резолверы ifunc, чтение /etc/ld.so.cache, и даёт сквозную интроспекцию: backtrace(), dladdr, dl_iterate_phdr и dladdr1 видят кадры и образы по обе стороны границы, а файловые маппинги остаются видны в /proc/self/maps для отладчиков. Также воспроизведены низкоуровневые части ABI glibc: getcontext/makecontext/swapcontext под реальный layout mcontext на обеих архитектурах, дореформенный (pre-2.34) ABI pthread, GNU obstacks, семейство функций _chk и inline-ABI stdio, структура FILE в musl нарочно устроена так, чтобы инлайновый putc_unlocked из glibc компилировался против неё и в итоге резолвился в поток самого musl.
У SoLo есть статический реестр провайдеров: приложение может заранее сказать, что зависимость системной .so (например, libwayland) должна закрываться символами, уже слинкованными в сам исполняемый файл, а не грузиться заново с диска, это позволяет встраивать более новую версию библиотеки вместо самой старой, что стоит в системе. Вне стандартных системных путей библиотеки ищутся через переменные окружения LD_LIBRARY_PATH и DL_ELF_LIBRARY_PATH.
Как демонстрацию проект собирает полностью статический исполняемый файл, который грузит немодифицированный Vulkan ICD-драйвер хоста, выполняет compute shader и записывает результат в PNG размером 512×512 в формате RGBA; для сборки задействован статически слинкованный libpng. Готовый бинарник можно скачать одной командой curl (vulkan-x86_64 и vulkan-aarch64 для ARM) и запустить на любом Linux с установленным Vulkan-драйвером (достаточно mesa-vulkan-drivers), а флагом --driver указать конкретный ICD-манифест (например, radeon_icd.x86_64.json или lvp_icd.json). Что бинарник действительно не имеет динамических связей, можно проверить самому: readelf -lW по нему не выводит INTERP, а readelf -dW сообщает об отсутствии секции dynamic. Проект собирается без CMake, Meson, configure или Make, build.py компилирует зависимости напрямую из вендоренных исходников: musl 1.2.5, рантаймы LLVM 15.0.7 (libc++, libc++abi, libunwind, compiler-rt), Vulkan Headers и Vulkan Loader версии 1.4.357, zlib 1.3.2 и libpng 1.6.50.
По утверждению README, демо проверено на GPU AMD (radv, radeonsi), Intel и NVIDIA под Linux, а также на Apple M1 под Asahi Linux; на каждый коммит CI дополнительно прогоняет SoLo через разделяемые библиотеки тысячи самых устанавливаемых пакетов Debian, более 2100 объектов хоста, на x86-64 и aarch64, а нативная сборка и тесты идут на Alpine/musl с GCC, на Fedora с GCC и на Ubuntu с Clang. Отдельно указано, что именно на SoLo и системе сборки статических бинарников IX построены релизные бинарники терминального эмулятора Shitty.
README сравнивает SoLo с несколькими существующими подходами. gcompat, дистрибутивная прослойка API glibc для запуска готовых glibc-бинарников на musl, перезапускает программу через штатный динамический линковщик musl с подгруженной libgcompat.so, но не даёт полностью статическому musl-процессу динамический загрузчик как таковой. Detour поднимает системный ld-linux и позволяет нескольким рантаймам C сосуществовать в процессе; Cosmopolitan Libc в cosmo_dlopen() следует той же схеме, поднимает ELF-интерпретатор и libc хоста и делегирует загрузку целевой .so системному dlopen(). Экспериментальный userspace-загрузчик ClickHouse сам маппит ELF-объекты, но пока не загружает glibc, а предложенный путь к системным библиотекам вроде CUDA описан как Detour-подобный. Эксперимент graphics.gd с musl+dlopen идёт по той же схеме раздельных рантаймов: встроенный помощник подтягивает загрузчик glibc хоста, а переходы между мирами musl и glibc обслуживают ассемблерные трамплины, из-за чего существуют два независимых мира TLS и коллбэк, реализованный в musl, нельзя безопасно передать в код glibc, пока активен его собственный TLS. SoLo вместо этого сам транслирует glibc-импорты на рантайм musl и не поднимает вторую libc и второй мир TLS в процессе вовсе. Отдельно README проводит границу с Flatpak, AppImage и контейнерами, которые решают проблему недостающей системной .so, пряча внутри или вокруг программы целый маленький дистрибутив Linux, с дублирующимися библиотеками, точками монтирования и пространствами имён, что усложняет профилирование, отладку и базовую интроспекцию; образно это сравнивается с переездом в новый дом ради решения проблемы отсутствующего переходника для розетки. Проект заявлен как рассчитанный только на Linux, x86-64 и aarch64.
Ключевые факты
- SoLo, собственный ELF-загрузчик (x86-64, aarch64) и мост glibc-ABI поверх musl: позволяет полностью статическому Linux-бинарнику вызывать dlopen()/dlsym() для системных .so, собранных под glibc, включая GPU-драйверы, без контейнера, AppImage и второй libc в процессе.
- Демо: статический бинарник грузит немодифицированный Vulkan-драйвер хоста, выполняет compute shader и пишет PNG 512×512 RGBA; заявлено тестирование на AMD radv/radeonsi, Intel и NVIDIA под Linux и на Apple M1 под Asahi Linux.
- На каждый коммит CI прогоняет SoLo через разделяемые библиотеки тысячи самых устанавливаемых Debian-пакетов, более 2100 объектов хоста, на x86-64 и aarch64; сборка и тесты также идут на Alpine/musl+GCC, Fedora+GCC и Ubuntu+Clang.
- Мост поддерживает межмировые C++-исключения, все четыре модели TLS, семантику связывания ld.so (RTLD_DEEPBIND, DT_SYMBOLIC, версионирование символов) и сквозную интроспекцию через backtrace()/dladdr/dl_iterate_phdr.
- README прямо сравнивает подход со связанными проектами, gcompat, Detour, cosmo_dlopen() из Cosmopolitan Libc, загрузчиком ClickHouse и экспериментом graphics.gd, и указывает, что все они держат в процессе вторую libc или второй мир TLS, а SoLo, нет.
Почему это важно
Полностью статические бинарники на musl, удобный способ разворачивать софт на Linux: один файл, без внешних зависимостей, ничего не ломается при обновлении дистрибутива. Но у этого удобства был жёсткий потолок: как только приложению нужен GPU, драйверы Vulkan и OpenGL поставляются хостом как разделяемые объекты, обычно собранные под glibc, а полностью статический musl-бинарник не может штатно вызвать для них dlopen(). До SoLo практичным ответом были контейнер, AppImage или запуск второй libc в процессе, SoLo убирает и то, и другое, оставляя один обычный, инспектируемый исполняемый файл, который занимает у хоста ровно то, что ему действительно нужно: аппаратно-специфичный драйвер.
Кому это важно
В первую очередь, разработчикам, которые собирают статические Linux-бинарники и упираются в GPU: игры, редакторы, инструменты для графики и вычислений, терминальные эмуляторы. В README прямо сказано, что именно на SoLo и системе сборки статических бинарников IX собираются релизные бинарники терминального эмулятора Shitty. Полезно и авторам систем контейнеризации/дистрибуции софта, которым важно избавиться от накладных расходов контейнера или AppImage там, где нужна лишь одна системная библиотека, а не целый маленький дистрибутив внутри.
Как это применить
SoLo поставляется как исходный API в стиле dlfcn (lib/dlfcn.h) поверх собственного ELF-загрузчика и ABI-моста; стандартная сборка даёт архив libdlfcn.a, который подключается к статическому musl-приложению, после этого обычные вызовы dlopen()/dlsym() перенаправляются на SoLo. Готовый демо-бинарник можно скачать без клонирования репозитория и тулчейна: curl -LO .../vulkan-x86_64 (или vulkan-aarch64 для ARM), затем ./vulkan-x86_64 hello.png, работает на любом Linux с установленным Vulkan-драйвером, достаточно пакета mesa-vulkan-drivers. Флаг --driver позволяет явно указать ICD-манифест конкретного драйвера, если нужен не тот, что находит стандартный поиск. Собрать демо из исходников можно через ./build vulkan при наличии Python 3 и компилятора C/C++; статический реестр провайдеров позволяет закрывать отдельные зависимости (например, Wayland) символами, уже слинкованными в сам исполняемый файл, вместо системной версии.
Можно ли доверять
У проекта есть заявленная и, по README, автоматизированная проверка: на каждый коммит CI прогоняет SoLo через разделяемые библиотеки тысячи самых устанавливаемых пакетов Debian (более 2100 объектов хоста) на x86-64 и aarch64, а нативные сборка и тесты идут на трёх разных связках дистрибутив+компилятор, Alpine/musl с GCC, Fedora с GCC, Ubuntu с Clang; заявлена также отдельная серия конформанс-тестов против заголовков настоящей glibc при -O2. Демо с GPU, по тому же README, проверено на драйверах AMD, Intel, NVIDIA и на Apple M1 под Asahi Linux. При этом это самоотчёт проекта, а не независимый аудит: в доступном тексте нет ни лицензии проекта, ни имени поддерживающего его человека или команды, ни отдельных измерений накладных расходов моста относительно обычного динамического связывания, судить о зрелости и правовом статусе стоит по самому репозиторию, а не только по описанию.
Риски и подводные камни
Проект заявлен только под Linux, x86-64 и aarch64, переносимости на другие ОС или архитектуры нет. В тексте не указана лицензия, поэтому условия использования и встраивания SoLo в чужой продукт нужно уточнять отдельно, а не считать их разрешёнными по умолчанию. Нет и опубликованных цифр по накладным расходам ABI-моста относительно родного динамического связывания, насколько дороже вызов функции через прослойку glibc_shim, из README не следует. Сам подход обязывает воспроизводить огромную поверхность ABI glibc вручную: часть функций явно не поддержана и при вызове аварийно завершает работу с именем символа, это безопаснее тихого повреждения памяти, но означает, что не любой сторонний .so гарантированно заработает без доработки моста. Наконец, широта заявленного тестирования (тысячи Debian-пакетов, несколько GPU-вендоров) проверяется только словами README, без независимого подтверждения.
«Это работает примерно так же, как переезд в новый дом решает проблему отсутствующего переходника для розетки.»
— README проекта SoLo, о подходе Flatpak, AppImage и контейнеров к той же проблеме