Por que um shell de login 'sudo -i' quebra um argumento de string de comando here-doc?

5

Na sequência de cinco comandos abaixo, todos dependem de aspas simples para distribuir a possível substituição de variável para o shell bash chamado, em vez do shell de chamada. O usuário chamador é xx , mas o shell chamado será executado como usuário yy . O primeiro comando substitui $ HOME pelo valor do shell de chamada porque o shell chamado não é um shell de login. O segundo comando substitui o valor de $ HOME carregado por um shell de login, então é o valor pertencente ao usuário yy . O terceiro comando não depende de um valor $ HOME e cria um arquivo no diretório inicial do usuário yy .

Por que o quarto comando falha? A intenção é que ele grave o mesmo arquivo, mas contando com a variável $ HOME pertencente ao usuário yy para garantir que ele realmente termine em seu diretório pessoal. Eu não entendo porque um shell de login quebra o comportamento de um comando here-doc passado como uma string de aspas simples estática. A falha do quinto comando verifica que esse problema não é sobre substituição de variáveis.

xx@host ~ $ sudo -u yy bash -c 'echo HOME=$HOME'
HOME=/home/xx
xx@host ~ $ sudo -iu yy bash -c 'echo HOME=$HOME'
HOME=/home/yy
xx@host ~ $ sudo -u yy bash -c 'cat > /home/yy/test.sh << "EOF"
> script-content
> EOF
> '
xx@host ~ $ sudo -iu yy bash -c 'cat > $HOME/test.sh << "EOF"
> script-content
> EOF
> '
bash: warning: here-document at line 0 delimited by end-of-file (wanted 'EOFscript-contentEOF')
xx@host ~ $ sudo -iu yy bash -c 'cat > /home/yy/test.sh << "EOF"
> script-content
> EOF
> '
bash: warning: here-document at line 0 delimited by end-of-file (wanted 'EOFscript-contentEOF')

Estes comandos foram emitidos em um sistema Linux Mint 18.3 Cinnamon 64-bit, que é baseado no Ubuntu 16.04 (Xenial Xerus).

Atualização: O aspecto aqui-doc está apenas nublando o problema. Aqui está uma simplificação do problema:

$ sudo bash -c 'echo 1
> echo 2'
1
2
$ sudo -i bash -c 'echo 1
> echo 2'
1echo 2

Por que o primeiro desses dois comandos preserva o linebreak e o segundo não? sudo é comum a ambos os comandos, mas parece estar escapando / filtrando / interpolando de forma diferente dependendo de nada além da opção "-i".

    
por froage 13.12.2017 / 02:00

2 respostas

8

A documentação para -i afirma:

The -i (simulate initial login) option runs the shell specified by the password database entry of the target user as a login shell. This means that login-specific resource files such as .profile or .login will be read by the shell. If a command is specified, it is passed to the shell for execution via the shell's -c option.

Ou seja, ele executa genuinamente o shell de login do usuário e, em seguida, passa o comando que você forneceu sudo para o usando -c - diferente de , que sudo cmd arg arg normalmente faz sem -i opção. Normalmente, sudo usa apenas uma das as funções exec* diretamente para iniciar o processo em si , sem shell intermediário e todos os argumentos passados exatamente como estão.

Com -i , ele configura o ambiente, executa o shell do usuário como um shell de login e reconstrói o comando que você solicitou para ser executado como um argumento para bash -c . No seu caso, ele é executado (digamos, aproximadamente) /bin/bash -c "bash -c ' ... '" (imagine citando que funciona).

O problema está em como sudo transforma o comando que você escreveu em algo que -c pode lidar com , explicado na próxima seção. A última seção tem algumas soluções possíveis, e entre algumas técnicas de depuração e verificação,

Por que isso acontece?

Ao passar o comando para -c , ele precisa de algum pré-processamento para fazer a coisa certa, o que sudo faz antes de executar o shell. Por exemplo, se seu comando for:

sudo -iu yy echo 'three   spaces'

então esses espaços precisam ser escapados para que o comando signifique a mesma coisa (ou seja, para o argumento único não ser dividido em duas palavras). O que acaba sendo executado é:

/bin/bash -c 'echo three\ \ \ spaces'

Vamos começar com o seu comando simplificado:

sudo bash -c 'echo 1
echo 2'

Nesse caso, sudo altera o usuário e, em seguida, executa execvp("bash", \["bash", "-c", "echo 1\necho 2"\]) (por uma sintaxe literal de array inventada).

com -i :

sudo -i bash -c 'echo 1
echo 2'

em vez disso, ele altera o usuário e executa execv("/bin/bash", ["-bash", "-c", "bash -c echo\ 1\\necho\ 2"]) , em que \ equivale a um literal \ e \n é um quebra de linha. Ele escapou dos espaços e da nova linha em seu comando principal, precedendo-os com barras invertidas.

Ou seja, há um shell de login externo, que possui dois argumentos: -c e todo o seu comando, reconstruídos em um formulário que se espera que o shell entenda corretamente. Infelizmente, isso não acontece. O comando interno bash tenta finalmente executar:

echo 1\
echo 2

em que a primeira linha física termina com uma continuação de linha (barra invertida seguida de nova linha), que é totalmente excluída. A linha lógica é apenas echo 1echo 2 , que não faz o que você queria.

Há um argumento de que essa é uma falha no escape de sudo , dado o padrão comportamento de pares de barra invertida-nova linha . Eu acho que deveria ser seguro deixá-los sem escape aqui.

O mesmo acontece para o seu comando com um documento aqui. Corre como, aproximadamente:

/bin/bash -c 'bash -c cat\ \<\<\ \"EOF\"\012script-content\012EOF\012'

em que 2 representa uma nova linha real - sudo inseriu uma barra invertida antes de cada uma delas, assim como os espaços. Observe o escape duplo na \012 : que é a versão ps de uma barra invertida real seguida de nova linha, que estou usando aqui (veja abaixo). O que eventualmente é executado é:

bash -c 'cat << "EOF"\
script-content\
EOF\
'

com continuações de linha \ + nova linha em todos os lugares, que são apenas removido. Isso faz com que seja uma linha longa, sem novas linhas reais, e um heredoc inválido:

bash -c 'cat << "EOF"script-contentEOF'

Então, esse é o seu problema: o processo bash interno recebe apenas uma linha de comando e, portanto, o documento aqui nunca tem a chance de terminar (ou iniciar). Eu tenho algumas soluções imperfeitas na parte inferior, mas esta é a causa subjacente.

Como você pode verificar o que está acontecendo?

Para obter esses comandos corretamente citados e validar o que estava acontecendo, modifiquei o arquivo de perfil do shell de login ( .profile , .bash_profile , .zprofile , etc) para dizer apenas:

ps awx|grep $$

Isso mostra a linha de comando do shell em execução no momento e me fornece um par extra de linhas de saída antes do aviso.

hexdump -C /proc/$$/cmdline também será útil no Linux.

O que você pode fazer sobre isso?

Eu não vejo uma maneira óbvia e confiável de conseguir o que você quer com isso. Você não quer que sudo toque em seu comando, se possível. Uma opção que funcionará em grande parte para um caso simples é canalizar os comandos para o shell, em vez de especificá-los na linha de comando:

printf 'cat > ... << ... \n ...' | sudo -iu yy

Isso requer cuidadoso escape interno ainda.

Provavelmente, é melhor colocá-los em um arquivo de script temporário e executá-los dessa maneira:

f='mktemp'
printf 'command' > "$f"
chmod +r "$f"
sudo -iu yy "$f"
rm "$f"

Um nome próprio criado por você também funcionará. Dependendo de suas configurações de sudoers, você poderá manter um descritor de arquivo aberto e fazer com que ele seja lido como um arquivo ( /dev/fd/3 ), se você realmente não o quiser no disco, mas um arquivo real será mais fácil.

    
por 13.12.2017 / 04:07
2

The more general need that brought this to light is to document sequences of commands that can be copy 'n pasted directly to a terminal session. Amongst these are commands that create small files in a manner that is human readable and still documents every step. Creating temporary files or directing the reader to "use an editor" deviates from the goal of what is essentially a 100% playback possibility. – froage 4 mins ago

Se for tudo o que você está tentando fazer, use:

sudo -u yy tee ~yy/test.sh >/dev/null <<'EOF'
script-content
EOF

Muito mais simples e você não precisa mexer com os problemas de colocar a coisa toda dentro de uma construção bash -c '...' . Especialmente se você tiver alguma citação no seu script, isso facilitará muito.

(Observe que a definição do delimitador heredoc é citada como 'EOF' , para que quaisquer variáveis usadas no conteúdo do script não sejam expandidas durante a criação de test.sh , o que seria provavelmente não será sua intenção.)

Observe também o uso da expansão do til para obter o diretório pessoal do usuário yy : ~yy . Veja LESS='+/^ *Tilde Expansion' man bash para saber mais sobre isso. É por isso que você não precisa de sudo -i .

    
por 13.12.2017 / 06:16