Nix necesita binarios reubicables
(fzakaria.com)- Nix, un gestor de paquetes basado en store, está diseñado para colocar los paquetes en un prefijo fijo como
/nix/store, lo que genera grandes limitaciones en entornos de Nix sin root que quieren ubicar el store en otro lugar sin una instalación previa de Nix ni privilegios de root - Si se usa
--store /tmp/...junto conchrooty un mount namespace, se puede mantener el mismo hash que en una compilación tradicional en/nix/store, y así seguir aprovechando cachés binarias comocache.nixos.org - Si se cambia el prefijo del store a
local?store=/tmp/...sin namespace, el hash cambia, y hasta una compilación simple dehellopuede invalidar todo el grafo de dependencias y llevar a recompilar GCC - El núcleo de la propuesta es usar rutas relativas basadas en
$ORIGIN, compatibles con el cargador dinámico de Linux, en elRUNPATHde ELF en vez de rutas absolutas, para evitar que mover el store termine propagándose a los hashes y las recompilaciones - El principal cuello de botella que impide una reubicación real es que el kernel no soporta
$ORIGINenPT_INTERPde ELF ni en los shebang de scripts; como vías de solución se proponen parches al kernel, wrappers estáticos, rutas relativas por lenguaje y metadatosrelocatable = true;
Choque entre un prefijo fijo del store y Nix sin root
- Los sistemas basados en store como Nix y Guix guardan todos los paquetes bajo un prefijo determinado
- Nix usa
/nix/store - Guix usa
/gnu/store
- Nix usa
- Esta estructura facilita reescribir rutas de binarios o bibliotecas
- Por ejemplo,
/bin/bashpuede reemplazarse por una ruta completa del store como/nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash
- Por ejemplo,
- También hay casos en los que se quiere ubicar el store en otro lugar
- Entornos donde Nix aún no está instalado
- Entornos donde no se tienen los permisos necesarios
- Ahí es donde aparece el problema de “Nix sin root”
- Nix ya permite indicar otra ruta para el store, pero según el método se conserva o no el hash
nix build nixpkgs#helloinstala en/nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/nix build --store /tmp/fzakaria/store nixpkgs#helloinstala en/tmp/fzakaria/store/nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/usandochrooty mount namespace- En ambos casos, el hash
zi2bj2hlavv8q743li2s9diqbcpmrf9bes el mismo
- Si el hash se mantiene, se pueden usar derivaciones precalculadas de un sustituidor binario como https://cache.nixos.org
El costo de cambiar el store sin namespace
- Herramientas como Bazel o Buck2 quizá ya usen namespaces para su propio sandboxing
- Si se intenta integrar Nix en esos ecosistemas, la anidación de user namespaces y las restricciones de mount reducen mucho la viabilidad práctica
- También se puede indicar un prefijo alternativo del store sin
chrootni mount namespace, pero eso tiene el defecto de cambiar el hash- El comando de ejemplo usa
--store 'local?store=/tmp/fzakaria/store&state=/tmp/fzakaria/state&log=/tmp/fzakaria/log' - La ruta resultante de
helloes/tmp/fzakaria/store/qv3fhi1j9gh27fyds5n5b16yia8i6zn5-hello-2.12.3 - El hash ya no es
zi2..., sinoqv3fhi1j9gh27fyds5n5b16yia8i6zn5
- El comando de ejemplo usa
- Un simple cambio en la cadena del prefijo del store provoca una invalidación en cascada de todo el grafo de dependencias
- Solo por querer imprimir “Hello World” desde otra carpeta, uno podría terminar compilando GCC durante 4 horas
- En ese caso no se puede aprovechar la caché pública
- Esta limitación ya está documentada en la documentación de Nix
Qué resuelve $ORIGIN y qué límites del kernel siguen ahí
- La causa del problema es que el prefijo del store forma parte de la derivación misma, así que afecta el cálculo del hash
- Si en lugar del prefijo completo del store se usan rutas relativas, se puede evitar el cambio de hash
- El
RUNPATHde un binario ELF es uno de los puntos donde esto puede aplicarse- Un ejemplo del
RUNPATHactual dehelloes/nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib - El cargador de Linux soporta
$ORIGIN, que apunta al directorio donde está el ejecutable - Por lo tanto, el
RUNPATHpodría escribirse como$ORIGIN/../../57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib - Así, aunque cambie la ubicación del store, no cambiaría el hash ni haría falta recompilar
- Un ejemplo del
- Pero antes de que el cargador dinámico lea el
RUNPATH, el kernel de Linux tiene que cargar primero al propio cargador dinámico- Esa ruta se guarda en el encabezado
PT_INTERPdel ELF - El ejemplo es
/nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib/ld-linux-x86-64.so.2 - Actualmente el kernel de Linux no soporta
$ORIGINenPT_INTERP
- Esa ruta se guarda en el encabezado
- Los shebang de scripts tienen la misma limitación
- El ejemplo es
#!/nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash - El kernel espera una ruta absoluta al parsear
#! - Hoy tampoco hay soporte para
$ORIGINen shebang
- El ejemplo es
- Se pueden usar rutas relativas basadas en el directorio de trabajo actual, pero no son confiables porque se rompen si el script se ejecuta desde otro lugar
Propuestas para llegar a binarios reubicables
- Para crear binarios realmente reubicables, hace falta esquivar o modificar las restricciones del kernel
- Se proponen tres enfoques
- Parchear el kernel de Linux para que soporte
$ORIGINenPT_INTERPy en shebang - Envolver todos los binarios con un pequeño binario estático, donde el wrapper calcule su propia ubicación y luego ejecute el cargador dinámico
- Cambiar también el acceso a archivos para aprovechar funciones de rutas relativas según el lenguaje
- En Python, por ejemplo,
__file__permite acceder a archivos tomando como base la ubicación del propio archivo
- En Python, por ejemplo,
- Parchear el kernel de Linux para que soporte
- La opción que se considera más adecuada es extender el soporte del kernel de Linux
- En máquinas con NixOS, se podría parchear el kernel con Nix para añadir ese soporte
- Además, se propone agregar a cada derivación un metadato
relocatable = true;para indicar si es reubicable
1 comentarios
Comentarios en Lobste.rs
Estaría bien que el kernel de Linux añadiera soporte para
$ORIGINenPT_INTERP. Hace tiempo probé con un binario wrapper estático, y he visto varios intentos más (buen ejemplo); todos son hacks excelentes e ingeniosos, pero al final siguen siendo hacks.Todavía no entiendo bien las implicaciones de seguridad, así que ayudaría tener una explicación ordenada.
Parece que Solaris sí lo soporta, así que quizá haya una manera de hacerlo de forma segura. Es difícil encontrar evidencia, pero en el ENOEXEC del manual de
execve(2)dice que falla si el encabezado de programaPT_INTERPdel archivo de imagen de proceso setuid/setgid tiene una ruta relativa o usa el token$ORIGINMucho software tiene rutas incrustadas en tiempo de compilación o constantes, así que para que funcione bien, ¿no habría que recompilarlo de todos modos?
outPathmediante{foo}, pero si cambias una de las dependencias superiores a un almacén local, hay que volver a compilar.El parche dcrt1 para musl (hecho por rcombs) resuelve este problema en espacio de usuario.
¿No se podría crear un sistema de archivos montado en
/originpara que se interprete como$ORIGIN? Entonces probablemente funcionaría tanto en shebang como en ELF sin sintaxis adicional./origin, entonces también podrías simplemente crear/nixy ejecutarnix-daemon, ¿no?Si un binario pudiera especificar un loader propio, quizá inseguro, mediante una ruta relativa, ¿no sería eso un riesgo de seguridad?
libc.so.6?