Sandbox

Este artigo aborda o uso de sandbox no Bazel e a depuração do ambiente de sandbox.

O uso de sandbox é uma estratégia de restrição de permissões que isola processos uns dos outros ou de recursos em um sistema. Para o Bazel, isso significa restringir o acesso ao sistema de arquivos.

O sandbox do sistema de arquivos do Bazel executa processos em um diretório de trabalho que contém apenas entradas conhecidas, de modo que os compiladores e outras ferramentas não vejam arquivos de origem a que não têm acesso, a menos que conheçam os caminhos absolutos para eles.

O uso de sandbox não oculta o ambiente do host de forma alguma. Os processos podem acessar livremente todos os arquivos no sistema de arquivos. No entanto, em plataformas que oferecem suporte a namespaces de usuário, os processos não podem modificar arquivos fora do diretório de trabalho. Isso garante que o gráfico de build não tenha dependências ocultas que possam afetar a capacidade de reprodução do build.

Mais especificamente, o Bazel cria um diretório execroot/ para cada ação, que atua como o diretório de trabalho da ação no momento da execução. execroot/ contém todos os arquivos de entrada da ação e serve como contêiner para todas as saídas geradas. Em seguida, o Bazel usa uma técnica fornecida pelo sistema operacional, contêineres no Linux e sandbox-exec no macOS, para restringir a ação em execroot/.

Motivos para usar o sandbox

  • Sem o uso de sandbox de ação, o Bazel não sabe se uma ferramenta usa arquivos de entrada não declarados (arquivos que não estão listados explicitamente nas dependências de uma ação). Quando um dos arquivos de entrada não declarados muda, o Bazel ainda acredita que o build está atualizado e não vai refazer a ação. Isso pode resultar em um build incremental incorreto.

  • A reutilização incorreta de entradas de cache cria problemas durante o armazenamento em cache remoto. Uma entrada de cache ruim em um cache compartilhado afeta todos os desenvolvedores do projeto, e limpar todo o cache remoto não é uma solução viável.

  • O uso de sandbox imita o comportamento da execução remota. Se um build funcionar bem com o uso de sandbox, provavelmente ele também vai funcionar com a execução remota. Ao fazer com que a execução remota faça upload de todos os arquivos necessários (incluindo ferramentas locais), é possível reduzir significativamente os custos de manutenção dos clusters de compilação em comparação com a instalação das ferramentas em cada máquina do cluster sempre que você quiser testar um novo compilador ou fazer uma mudança em uma ferramenta atual.

Qual estratégia de sandbox usar

É possível escolher qual tipo de sandbox usar, se houver, com as flags de estratégia. Usar a estratégia sandboxed faz com que o Bazel escolha uma das implementações de sandbox listadas abaixo, preferindo um sandbox específico do SO ao genérico menos hermético. Os workers persistentes são executados em um sandbox genérico se você transmitir a flag --worker_sandboxing.

A estratégia local (também conhecida como standalone) não faz nenhum tipo de sandbox. Ela simplesmente executa a linha de comando da ação com o diretório de trabalho definido como o execroot do seu espaço de trabalho.

processwrapper-sandbox é uma estratégia de sandbox que não exige recursos "avançados". Ela funciona em qualquer sistema POSIX. Ela cria um diretório de sandbox que consiste em links simbólicos que apontam para os arquivos de origem originais, executa a linha de comando da ação com o diretório de trabalho definido para esse diretório em vez do execroot, move os artefatos de saída conhecidos do sandbox para o execroot e exclui o sandbox. Isso impede que a ação use acidentalmente arquivos de entrada não declarados e que o execroot seja poluído com arquivos de saída desconhecidos.

linux-sandbox vai um passo além e é criado com base no processwrapper-sandbox. Semelhante ao que o Docker faz nos bastidores, ele usa namespaces do Linux (namespaces de usuário, montagem, PID, rede e IPC) para isolar a ação do host. Ou seja, ele torna todo o sistema de arquivos somente leitura, exceto o diretório de sandbox, para que a ação não possa modificar nada acidentalmente no sistema de arquivos do host. Isso evita situações como um teste com bugs que rm -rf'ing acidentalmente seu diretório $HOME. Opcionalmente, também é possível impedir que a ação acesse a rede. linux-sandbox usa namespaces de PID para impedir que a ação veja outros processos e para encerrar de forma confiável todos os processos (até mesmo daemons gerados pela ação) no final.

darwin-sandbox é semelhante, mas para macOS. Ele usa a ferramenta sandbox-exec da Apple para alcançar aproximadamente o mesmo que o sandbox do Linux.

O linux-sandbox e o darwin-sandbox não funcionam em um cenário "aninhado" devido a restrições nos mecanismos fornecidos pelos sistemas operacionais. Como o Docker também usa namespaces do Linux para a mágica do contêiner, não é possível executar linux-sandbox facilmente dentro de um contêiner do Docker, a menos que você use docker run --privileged. No macOS, não é possível executar sandbox-exec dentro de um processo que já está sendo usado no sandbox. Assim, nesses casos, o Bazel volta automaticamente a usar processwrapper-sandbox.

Se você preferir receber um erro de build, por exemplo, para não criar acidentalmente um build com uma estratégia de execução menos rigorosa, modifique explicitamente a lista de estratégias de execução que o Bazel tenta usar (por exemplo, bazel build --spawn_strategy=worker,linux-sandbox).

A execução dinâmica geralmente exige o uso de sandbox para execução local. Para desativar, transmita a flag --experimental_local_lockfree_output. A execução dinâmica usa o sandbox de workers persistentes de forma silenciosa persistent workers.

Desvantagens do uso de sandbox

  • O uso de sandbox gera custos extras de configuração e desmontagem. O tamanho desse custo depende de muitos fatores, incluindo o formato do build e a performance do SO do host. Para o Linux, os builds de sandbox raramente são mais lentos do que alguns pontos percentuais. Definir --reuse_sandbox_directories pode atenuar o custo de configuração e desmontagem.

  • O uso de sandbox desativa efetivamente qualquer cache que a ferramenta possa ter. É possível atenuar isso usando workers persistentes, ao custo de garantias de sandbox mais fracas.

  • Os workers multiplex exigem suporte explícito para serem usados no sandbox. Os workers que não oferecem suporte ao uso de sandbox multiplex são executados como workers singleplex em execução dinâmica, o que pode custar mais memória.

Depuração

Siga as estratégias abaixo para depurar problemas com o uso de sandbox.

Namespaces desativados

Em algumas plataformas, como Google Kubernetes Engine nós de cluster ou Debian, os namespaces de usuário são desativados por padrão devido a problemas de segurança. Se o arquivo /proc/sys/kernel/unprivileged_userns_clone existir e contiver um 0, ative os namespaces de usuário executando:

   sudo sysctl kernel.unprivileged_userns_clone=1

Falhas na execução de regras

O sandbox pode falhar ao executar regras devido à configuração do sistema. Se você receber uma mensagem como namespace-sandbox.c:633: execvp(argv[0], argv): No such file or directory, tente desativar o sandbox com --strategy=Genrule=local para genrules e --spawn_strategy=local para outras regras.

Depuração detalhada para falhas de build

Se o build falhou, use --verbose_failures e --sandbox_debug para que o Bazel mostre o comando exato executado quando o build falhou, incluindo a parte que configura o sandbox.

Exemplo de mensagem de erro:

ERROR: path/to/your/project/BUILD:1:1: compilation of rule
'//path/to/your/project:all' failed:

Sandboxed execution failed, which may be legitimate (such as a compiler error),
or due to missing dependencies. To enter the sandbox environment for easier
debugging, run the following command in parentheses. On command failure, a bash
shell running inside the sandbox will then automatically be spawned

namespace-sandbox failed: error executing command
  (cd /some/path && \
  exec env - \
    LANG=en_US \
    PATH=/some/path/bin:/bin:/usr/bin \
    PYTHONPATH=/usr/local/some/path \
  /some/path/namespace-sandbox @/sandbox/root/path/this-sandbox-name.params --
  /some/path/to/your/some-compiler --some-params some-target)

Agora é possível inspecionar o diretório de sandbox gerado e ver quais arquivos o Bazel criou e executar o comando novamente para conferir o comportamento dele.

O Bazel não exclui o diretório de sandbox quando você usa --sandbox_debug. A menos que você esteja depurando ativamente, desative --sandbox_debug, porque ele preenche o disco ao longo do tempo.