Nix precisa de binários relocáveis
(fzakaria.com)- O Nix, um gerenciador de pacotes baseado em store, foi projetado para colocar pacotes em um prefixo fixo como
/nix/store, o que cria fortes limitações em ambientes de Nix sem root que querem usar o store em outro local sem uma instalação prévia do Nix nem privilégios de root - Ao usar
--store /tmp/...junto comchroote namespace de montagem, é possível manter o mesmo hash de builds feitos em/nix/store, continuando a aproveitar caches binários comocache.nixos.org - Se o prefixo do store for alterado com
local?store=/tmp/...sem namespace, o hash muda, e até um build simples dehellopode invalidar toda a árvore de dependências e levar à recompilação do GCC - O núcleo da proposta é usar caminhos relativos baseados em
$ORIGIN, suportados pelo linker dinâmico do Linux, noRUNPATHdo ELF em vez de caminhos absolutos, para que a mudança de local do store não se propague em forma de hashes diferentes e recompilações - O principal gargalo que impede a relocação real é que o kernel não oferece suporte a
$ORIGINnoPT_INTERPde ELF nem em shebangs de scripts; como caminho de solução, são propostos patch no kernel, wrappers estáticos, caminhos relativos específicos por linguagem e metadadosrelocatable = true;
O conflito entre prefixo fixo do store e Nix sem root
- Sistemas baseados em store como Nix e Guix armazenam todos os pacotes sob um prefixo definido
- Nix usa
/nix/store - Guix usa
/gnu/store
- Nix usa
- Essa estrutura facilita reescrever caminhos de binários e bibliotecas
- Por exemplo,
/bin/bashpode ser trocado por um caminho completo do store como/nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash
- Por exemplo,
- Há casos em que se quer colocar o store em outro local
- Ambientes em que o Nix ainda não está instalado
- Ambientes sem as permissões necessárias
- Nesses casos surge o problema do “Nix sem root”
- Hoje o Nix já permite definir outro caminho de store, mas a preservação do hash depende da forma usada
nix build nixpkgs#helloinstala em/nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/nix build --store /tmp/fzakaria/store nixpkgs#hellousachroote namespace de montagem para instalar em/tmp/fzakaria/store/nix/store/zi2bj2hlavv8q743li2s9diqbcpmrf9b-hello-2.12.3/- Nos dois casos, o hash
zi2bj2hlavv8q743li2s9diqbcpmrf9bpermanece igual
- Quando o hash é o mesmo, é possível usar derivations pré-calculadas de substituidores binários como https://cache.nixos.org
O custo de mudar o store sem namespace
- Ferramentas como Bazel e Buck2 podem já usar namespaces para seu próprio sandboxing
- Ao tentar integrar o Nix a esse ecossistema, restrições de namespaces de usuário aninhados e de montagem podem prejudicar bastante a viabilidade prática
- Também é possível definir um prefixo alternativo de store sem
chrootnem namespace de montagem, mas isso traz o problema da mudança de hash- O comando de exemplo usa
--store 'local?store=/tmp/fzakaria/store&state=/tmp/fzakaria/state&log=/tmp/fzakaria/log' - O caminho resultante para
hellovira/tmp/fzakaria/store/qv3fhi1j9gh27fyds5n5b16yia8i6zn5-hello-2.12.3 - O hash deixa de ser
zi2...e passa a serqv3fhi1j9gh27fyds5n5b16yia8i6zn5
- O comando de exemplo usa
- A simples troca da string do prefixo do store invalida em cascata toda a árvore de dependências
- Só para imprimir “Hello World” a partir de outra pasta, pode-se acabar compilando o GCC por 4 horas
- Nesse cenário, não é possível aproveitar caches públicos
- Essa limitação já está documentada no manual do Nix
O que o $ORIGIN resolve e o limite que ainda fica no kernel
- A origem do problema é que o prefixo do store faz parte da própria derivation, influenciando o cálculo do hash
- Se, em vez de usar o prefixo completo do store em toda parte, forem usados caminhos relativos, a mudança de hash pode ser evitada
- O
RUNPATHde binários ELF é um dos pontos em que isso pode ser aplicado- Um exemplo atual de
RUNPATHemhelloé/nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib - O loader do Linux suporta
$ORIGIN, que representa o diretório do executável - Assim, o
RUNPATHpode ser escrito como$ORIGIN/../../57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib - Com isso, o local do store pode mudar sem alterar o hash nem exigir recompilação
- Um exemplo atual de
- Porém, antes de o linker dinâmico ler o
RUNPATH, o kernel do Linux precisa primeiro carregar o próprio linker dinâmico- Esse caminho fica armazenado no cabeçalho
PT_INTERPdo ELF - O exemplo é
/nix/store/57iz36553175g3178pvxjij8z5rcsd4n-glibc-2.42-61/lib/ld-linux-x86-64.so.2 - Hoje o kernel do Linux não oferece suporte a
$ORIGINemPT_INTERP
- Esse caminho fica armazenado no cabeçalho
- Shebangs de scripts sofrem da mesma limitação
- Um exemplo é
#!/nix/store/gik3rh1vz2jlgnifb9dh6vc6sxwwz9jj-bash-5.3p9/bin/bash - Ao fazer o parse de
#!, o kernel espera um caminho absoluto - Atualmente também não há suporte a
$ORIGINem shebangs
- Um exemplo é
- Caminhos relativos com base no diretório de trabalho atual até podem ser usados, mas se o script for executado de outro lugar tudo quebra, então não são confiáveis
Propostas para chegar a binários relocáveis
- Para criar binários realmente relocáveis, é preciso contornar ou alterar as restrições do kernel
- Três abordagens são propostas
- Fazer patch no kernel Linux para suportar
$ORIGINemPT_INTERPe em shebangs - Envolver todos os binários em um pequeno binário estático, que calcula sua própria localização e depois executa o linker dinâmico
- Passar a usar também recursos de caminhos relativos específicos de cada linguagem
- Em Python, por exemplo,
__file__permite acessar arquivos com base na localização do próprio arquivo
- Em Python, por exemplo,
- Fazer patch no kernel Linux para suportar
- A abordagem apontada como mais adequada é estender o suporte no kernel Linux
- Em máquinas NixOS, seria possível aplicar esse patch no kernel via Nix
- Além disso, propõe-se adicionar a cada derivation um metadado
relocatable = true;para indicar se ela é relocável
1 comentários
Comentários no Lobste.rs
Seria bom se o kernel Linux passasse a oferecer suporte a
$ORIGINemPT_INTERP. Já tentei isso antes com um binário wrapper estático, e vi várias outras tentativas (bom exemplo); todas são hacks excelentes e elegantes, mas no fim continuam sendo hacksAinda não entendi direito as implicações de segurança, então seria útil ter uma explicação bem organizada
Parece que o Solaris oferece suporte a isso, então talvez exista uma forma segura de fazer. É difícil encontrar referências, mas no ENOEXEC do manual de
execve(2)diz que falha se o cabeçalho de programaPT_INTERPdo arquivo de imagem de processo setuid/setgid tiver um caminho relativo ou usar o token$ORIGINMuito software tem caminhos embutidos no momento do build ou constantes, então no fim não seria preciso recompilar de qualquer forma para funcionar direito?
outPathvia{foo}, mas se uma das dependências superiores for trocada por um repositório local, vai ser preciso reconstruirO patch dcrt1 para musl (feito por rcombs) resolve esse problema em espaço de usuário
Não daria para criar um sistema de arquivos montado em
/originpara que ele fosse interpretado como$ORIGIN? Aí isso funcionaria tanto em shebang quanto em ELF, sem sintaxe adicional/origin, então não poderia simplesmente criar/nixe executar onix-daemon?Não seria um risco de segurança se o binário pudesse apontar para um loader próprio usando um caminho relativo, provavelmente inseguro?
libc.so.6?