1 puntos por GN⁺ 3 시간 전 | 1 comentarios | Compartir por WhatsApp
  • 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 con chroot y 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 como cache.nixos.org
  • Si se cambia el prefijo del store a local?store=/tmp/... sin namespace, el hash cambia, y hasta una compilación simple de hello puede 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 el RUNPATH de 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 $ORIGIN en PT_INTERP de 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 metadatos relocatable = 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
  • Esta estructura facilita reescribir rutas de binarios o bibliotecas
    • Por ejemplo, /bin/bash puede reemplazarse por una ruta completa del store como /nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash
  • 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#hello instala en /nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/
    • nix build --store /tmp/fzakaria/store nixpkgs#hello instala en /tmp/fzakaria/store/nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/ usando chroot y mount namespace
    • En ambos casos, el hash zi2bj2hlavv8q743li2s9diqbcpmrf9b es 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 chroot ni 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 hello es /tmp/fzakaria/store/qv3fhi1j9gh27fyds5n5b16yia8i6zn5-hello-2.12.3
    • El hash ya no es zi2..., sino qv3fhi1j9gh27fyds5n5b16yia8i6zn5
  • 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 RUNPATH de un binario ELF es uno de los puntos donde esto puede aplicarse
    • Un ejemplo del RUNPATH actual de hello es /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 RUNPATH podrí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
  • 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_INTERP del 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 $ORIGIN en PT_INTERP
  • 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 $ORIGIN en shebang
  • 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 $ORIGIN en PT_INTERP y 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
  • 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

 
GN⁺ 3 시간 전
Comentarios en Lobste.rs
  • Estaría bien que el kernel de Linux añadiera soporte para $ORIGIN en PT_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 programa PT_INTERP del archivo de imagen de proceso setuid/setgid tiene una ruta relativa o usa el token $ORIGIN

  • Mucho 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?

    • Eso ya pasa bastante, sobre todo en las líneas shebang. Nix incluye muchas herramientas para sustituir esos valores en tiempo de compilación.
    • Sí, pero este problema siempre ha existido, independientemente del almacén local. Probablemente las derivaciones de nivel inferior consuman outPath mediante {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 /origin para que se interprete como $ORIGIN? Entonces probablemente funcionaría tanto en shebang como en ELF sin sintaxis adicional.

    • Si puedes crear y montar /origin, entonces también podrías simplemente crear /nix y ejecutar nix-daemon, ¿no?
    • Creo que, para que sea compatible también fuera de NixOS, la idea sería evitar instalar o configurar sistemas de archivos o demonios adicionales.
  • Si un binario pudiera especificar un loader propio, quizá inseguro, mediante una ruta relativa, ¿no sería eso un riesgo de seguridad?

    • En ese modelo de amenaza, ¿qué se está considerando confiable y qué no? Si de todos modos vas a ejecutar ese binario, ¿la idea es solo que se ejecute con un loader verificado?
    • ¿Por qué habría que considerarlo menos seguro que las variables de entorno que determinan la ruta de búsqueda de otras bibliotecas de enlace dinámico como libc.so.6?