Por que printf com um único argumento (sem especificadores de conversão) foi descontinuado?

102

Em um livro que estou lendo, está escrito que printfcom um único argumento (sem especificadores de conversão) está obsoleto. Recomenda substituir

printf("Hello World!");

com

puts("Hello World!");

ou

printf("%s", "Hello World!");

Alguém pode me dizer por que printf("Hello World!");está errado? Está escrito no livro que contém vulnerabilidades. Quais são essas vulnerabilidades?

StackUser
fonte
34
Nota: nãoprintf("Hello World!") é o mesmo que . anexa a . Em vez disso, compare computs("Hello World!")puts()'\n'printf("abc")fputs("abc", stdout)
chux - Reintegrar Monica
5
Que livro é esse? Não acho que printfesteja obsoleto da mesma forma que, por exemplo, getsestá obsoleto no C99, então você pode considerar a edição de sua pergunta para ser mais preciso.
el.pescado
14
Parece que o livro que você está lendo não é muito bom - um bom livro não deve apenas dizer que algo como isso é "obsoleto" (isso é factualmente falso, a menos que o autor esteja usando a palavra para descrever sua própria opinião) e deve explicar qual uso é realmente inválido e perigoso ao invés de mostrar um código seguro / válido como um exemplo de algo que você "não deveria fazer".
R .. GitHub PARAR DE AJUDAR O ICE
8
Você consegue identificar o livro?
Keith Thompson
7
Especifique o título do livro, autor e referência da página. THX.
Greenonline

Respostas:

122

printf("Hello World!"); IMHO não é vulnerável, mas considere o seguinte:

const char *str;
...
printf(str);

Se stracontecer de apontar para uma string contendo %sespecificadores de formato, seu programa exibirá um comportamento indefinido (principalmente uma falha), enquanto puts(str)apenas exibirá a string como está.

Exemplo:

printf("%s");   //undefined behaviour (mostly crash)
puts("%s");     // displays "%s\n"
Jabberwocky
fonte
21
Além de causar o travamento do programa, existem muitas outras explorações possíveis com strings de formato. Veja aqui para mais informações: en.wikipedia.org/wiki/Uncontrolled_format_string
e.dan
9
Outra razão é que putsprovavelmente será mais rápido.
edmz
38
@black: putsé "presumivelmente" mais rápido, e esse é provavelmente outro motivo pelo qual as pessoas o recomendam, mas não é realmente mais rápido. Acabei de imprimir "Hello, world!"1.000.000 de vezes, dos dois lados. Com printfele demorou 0,92 segundos. Com putsele demorou 0,93 segundos. Há coisas com que se preocupar quando se trata de eficiência, mas o printfvs. putsnão é uma delas.
Steve Summit
10
@KonstantinWeitz: Mas (a) eu não estava usando o gcc e (b) não importa por que a afirmação " putsé mais rápido" é falsa, ainda é falsa.
Steve Summit
6
@KonstantinWeitz: A alegação que forneci evidências era (o oposto) a alegação que o usuário black estava fazendo. Estou apenas tentando esclarecer que os programadores não devem se preocupar em ligar putspor esse motivo. (Mas se você quisesse discutir sobre isso: eu ficaria surpreso se você pudesse encontrar qualquer compilador moderno para qualquer máquina moderna onde putsseja significativamente mais rápido do que printfem qualquer circunstância.)
Steve Summit
75

printf("Hello world");

está bem e não tem vulnerabilidade de segurança.

O problema está em:

printf(p);

onde pé um ponteiro para uma entrada controlada pelo usuário. É propenso a ataques de strings de formato : o usuário pode inserir especificações de conversão para assumir o controle do programa, por exemplo, %xpara despejar a memória ou %nsobrescrever a memória.

Observe que puts("Hello world")não é equivalente em comportamento a, printf("Hello world")mas a printf("Hello world\n"). Os compiladores geralmente são inteligentes o suficiente para otimizar a última chamada para substituí-la puts.

ouah
fonte
10
É claro printf(p,x)que seria igualmente problemático se o usuário tivesse controle sobre p. Portanto, o problema não é o uso de printfcom apenas um argumento, mas sim com uma string de formato controlada pelo usuário.
Hagen von Eitzen
2
@HagenvonEitzen Isso é tecnicamente verdade, mas poucos usariam deliberadamente uma string de formato fornecida pelo usuário. Quando as pessoas escrevem printf(p), é porque não percebem que é uma string de formato, elas apenas pensam que estão imprimindo um literal.
Barmar de
33

Além das outras respostas, printf("Hello world! I am 50% happy today")é um bug fácil de fazer, potencialmente causando todos os tipos de problemas de memória desagradáveis ​​(é UB!).

É apenas mais simples, fácil e mais robusto "exigir" que os programadores sejam absolutamente claros quando desejam uma string literal e nada mais .

E é isso que printf("%s", "Hello world! I am 50% happy today")você pega. É totalmente à prova de falhas.

(Steve, printf("He has %d cherries\n", ncherries)é claro que não é absolutamente a mesma coisa; neste caso, o programador não está na mentalidade de "string literal"; ela está na mentalidade de "format string".)

Lightness Races in Orbit
fonte
2
Isso não vale a pena discutir, e eu entendo o que você está dizendo sobre a mentalidade literal versus formato de string, mas, bem, nem todo mundo pensa assim, o que é uma das razões pelas quais regras de tamanho único podem irritar. Dizer "nunca imprimir strings constantes com printf" é exatamente como dizer "sempre escreva if(NULL == p). Essas regras podem ser úteis para alguns programadores, mas não para todos. E em ambos os casos ( printfformatos incompatíveis e condicionais Yoda), os compiladores modernos avisam sobre erros de qualquer maneira, portanto, as regras artificiais são ainda menos importantes.
Steve Summit
1
@Steve Se houver exatamente zero vantagens em usar algo, mas algumas desvantagens, então sim, não há realmente nenhuma razão para usá-lo. As condições do Yoda, por outro lado , têm a desvantagem de tornar o código mais difícil de ler (você diria intuitivamente "se p for zero" e não "se zero for p").
Voo de
2
@Voo printf("%s", "hello")vai ser mais lento do que printf("hello"), então há uma desvantagem. Um pequeno, porque IO é quase sempre muito mais lento do que uma formatação simples, mas é uma desvantagem.
Yakk - Adam Nevraumont
1
@Yakk, duvido que seria mais lento
MM
gcc -Wall -W -Werrorirá prevenir consequências ruins de tais erros.
chqrlie
17

Vou apenas adicionar algumas informações sobre a parte da vulnerabilidade aqui.

Diz-se que é vulnerável devido à vulnerabilidade do formato de string printf. Em seu exemplo, onde a string é codificada, é inofensiva (mesmo se a codificação de strings como esta nunca for totalmente recomendada). Mas especificar os tipos de parâmetro é um bom hábito a se tomar. Veja este exemplo:

Se alguém colocar o caractere da string de formato em seu printf ao invés de uma string regular (digamos, se você quiser imprimir o programa stdin), printf pegará tudo o que puder na pilha.

Foi (e ainda é) muito usado para explorar programas para explorar pilhas para acessar informações ocultas ou contornar a autenticação, por exemplo.

Exemplo (C):

int main(int argc, char *argv[])
{
    printf(argv[argc - 1]); // takes the first argument if it exists
}

se eu colocar como entrada deste programa "%08x %08x %08x %08x %08x\n"

printf ("%08x %08x %08x %08x %08x\n"); 

Isso instrui a função printf a recuperar cinco parâmetros da pilha e exibi-los como números hexadecimais preenchidos com 8 dígitos. Portanto, uma possível saída pode ser:

40012980 080628c4 bffff7a4 00000005 08059c04

Veja isto para uma explicação mais completa e outros exemplos.

P1kachu
fonte
13

Chamar printfcom strings de formato literal é seguro e eficiente, e existem ferramentas para avisá-lo automaticamente se a invocação de printfcom strings de formato fornecidas pelo usuário não for segura.

Os ataques mais severos em printftiram vantagem do %nespecificador de formato. Em contraste com todos os outros especificadores de formato, por exemplo %d, %nrealmente grava um valor em um endereço de memória fornecido em um dos argumentos de formato. Isso significa que um invasor pode sobrescrever a memória e, portanto, potencialmente assumir o controle de seu programa. A Wikipedia fornece mais detalhes.

Se você chamar printfcom uma string de formato literal, um invasor não poderá %ninserir um em sua string de formato e, portanto, você estará seguro. Na verdade, o gcc mudará sua chamada para printfem uma chamada para puts, então literalmente não há nenhuma diferença (teste executando gcc -O3 -S).

Se você chamar printfcom uma string de formato fornecida pelo usuário, um invasor pode potencialmente %ninserir um em sua string de formato e assumir o controle de seu programa. Seu compilador geralmente irá avisá-lo de que o dele não é seguro, veja -Wformat-security. Existem também ferramentas mais avançadas que garantem que uma invocação de printfé segura, mesmo com strings de formato fornecidas pelo usuário, e podem até verificar se você passou o número e o tipo correto de argumentos para printf. Por exemplo, para Java, existe o Google's Error Prone e o Checker Framework .

Konstantin Weitz
fonte
12

Este é um conselho equivocado. Sim, se você tiver uma string de tempo de execução para imprimir,

printf(str);

é muito perigoso, e você deve sempre usar

printf("%s", str);

em vez disso, porque em geral você nunca sabe se strpode conter um %sinal. No entanto, se você tiver uma string constante de tempo de compilação , não há nada de errado com

printf("Hello, world!\n");

(Entre outras coisas, esse é o programa C mais clássico de todos os tempos, literalmente do livro de programação C do Gênesis. Portanto, qualquer pessoa que rejeite esse uso está sendo um tanto herética, e eu ficaria um pouco ofendido!)

Steve Summit
fonte
because printf's first argument is always a constant stringNão tenho certeza do que você quer dizer com isso.
Sebastian Mach
Como eu disse, "He has %d cherries\n"é uma string constante, o que significa que é uma constante de tempo de compilação. Mas, para ser justo, o conselho do autor não era "não passe strings constantes como printfo primeiro argumento", era "não passe strings sem %como printfprimeiro argumento".
Steve Summit
literally from the C programming book of Genesis. Anyone deprecating that usage is being quite offensively heretical- você realmente não leu K&R nos últimos anos. Há uma tonelada de conselhos e estilos de codificação que não apenas foram descontinuados, mas são apenas práticas inadequadas atualmente.
Voo de
@Voo: Bem, vamos apenas dizer que nem tudo que é considerado uma prática ruim é, na verdade, uma prática ruim. (O conselho de "nunca usar simplesmente int" vem à mente.)
Steve Summit
1
@Steve Não tenho ideia de onde você ouviu isso, mas certamente não é o tipo de prática ruim (ruim?) De que estamos falando. Não me entenda mal, por enquanto o código estava perfeitamente bom, mas você realmente não quer olhar para k & r muito, mas como uma nota histórica nos dias de hoje. "Está em k e r" simplesmente não é um indicador de boa qualidade nos dias de hoje, só isso
Voo
9

Um aspecto bastante desagradável de printfé que mesmo em plataformas onde as leituras de memória perdida só podem causar danos limitados (e aceitáveis), um dos caracteres de formatação %n, faz com que o próximo argumento seja interpretado como um ponteiro para um inteiro gravável e faz com que o número de caracteres produzidos até agora para serem armazenados na variável identificada. Eu nunca usei esse recurso, e às vezes eu uso métodos leves do estilo printf que escrevi para incluir apenas os recursos que eu realmente uso (e não incluo aquele ou algo semelhante), mas alimentando strings de funções de printf padrão recebidas de fontes não confiáveis ​​podem expor vulnerabilidades de segurança além da capacidade de ler armazenamento arbitrário.

supergato
fonte
8

Já que ninguém mencionou, gostaria de acrescentar uma nota sobre o desempenho deles.

Em circunstâncias normais, supondo que nenhuma otimização do compilador seja usada (ou seja, printf()realmente chamadas printf()e não fputs()), eu esperaria um printf()desempenho menos eficiente, especialmente para strings longas. Isso ocorre porque printf()é necessário analisar a string para verificar se há algum especificador de conversão.

Para confirmar isso, fiz alguns testes. O teste é realizado no Ubuntu 14.04, com gcc 4.8.4. Minha máquina usa uma CPU Intel i5. O programa que está sendo testado é o seguinte:

#include <stdio.h>
int main() {
    int count = 10000000;
    while(count--) {
        // either
        printf("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM");
        // or
        fputs("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM", stdout);
    }
    fflush(stdout);
    return 0;
}

Ambos são compilados com gcc -Wall -O0. O tempo é medido usando time ./a.out > /dev/null. O seguinte é o resultado de uma execução típica (eu os executei cinco vezes, todos os resultados estão dentro de 0,002 segundos).

Para a printf()variante:

real    0m0.416s
user    0m0.384s
sys     0m0.033s

Para a fputs()variante:

real    0m0.297s
user    0m0.265s
sys     0m0.032s

Este efeito é amplificado se você tiver uma corda muito longa.

#include <stdio.h>
#define STR "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM"
#define STR2 STR STR
#define STR4 STR2 STR2
#define STR8 STR4 STR4
#define STR16 STR8 STR8
#define STR32 STR16 STR16
#define STR64 STR32 STR32
#define STR128 STR64 STR64
#define STR256 STR128 STR128
#define STR512 STR256 STR256
#define STR1024 STR512 STR512
int main() {
    int count = 10000000;
    while(count--) {
        // either
        printf(STR1024);
        // or
        fputs(STR1024, stdout);
    }
    fflush(stdout);
    return 0;
}

Para a printf()variante (executado três vezes, real mais / menos 1,5s):

real    0m39.259s
user    0m34.445s
sys     0m4.839s

Para a fputs()variante (executado três vezes, real mais / menos 0,2s):

real    0m12.726s
user    0m8.152s
sys     0m4.581s

Nota: Depois de inspecionar o assembly gerado pelo gcc, percebi que o gcc otimiza a fputs()chamada para uma fwrite()chamada, mesmo com -O0. (A printf()chamada permanece inalterada.) Não tenho certeza se isso invalidará meu teste, pois o compilador calcula o comprimento da string fwrite()no momento da compilação.

user12205
fonte
2
Isso não invalidará seu teste, como fputs()costuma ser usado com constantes de string e essa oportunidade de otimização é parte do ponto que você queria fazer. Dito isso, adicionar uma execução de teste com uma string gerada dinamicamente com fputs()e fprintf()seria um bom ponto de dados suplementar .
Patrick Schlüter
@PatrickSchlüter Testar com strings geradas dinamicamente parece anular o propósito desta questão ... OP parece estar interessado apenas em strings literais a serem impressas.
user12205
1
Ele não afirma isso explicitamente, mesmo que seu exemplo use literais de string. Na verdade, acho que sua confusão sobre o conselho do livro é resultado do uso de literais de string no exemplo. Com strings literais, o conselho do livro é um tanto duvidoso, com strings dinâmicas é um bom conselho.
Patrick Schlüter
1
/dev/nullmeio que torna isso um brinquedo, pois geralmente ao gerar uma saída formatada, seu objetivo é que a saída vá para algum lugar, não seja descartada. Depois de adicionar o tempo "na verdade, não descartando os dados", como eles se comparam?
Yakk - Adam Nevraumont
7
printf("Hello World\n")

compila automaticamente para o equivalente

puts("Hello World")

você pode verificá-lo desmontando seu executável:

push rbp
mov rbp,rsp
mov edi,str.Helloworld!
call dword imp.puts
mov eax,0x0
pop rbp
ret

usando

char *variable;
... 
printf(variable)

levará a problemas de segurança, nunca use o printf dessa forma!

então seu livro está realmente correto, usar printf com uma variável está obsoleto, mas você ainda pode usar printf ("minha string \ n") porque ele se tornará automaticamente puts

Ábrahám Endre
fonte
12
Na verdade, esse comportamento depende inteiramente do compilador.
Jabberwocky
6
Isso é enganoso. Você afirma A compiles to B, mas na realidade você quer dizer A and B compile to C.
Sebastian Mach
6

Para gcc, é possível habilitar avisos específicos para verificação printf()e scanf().

A documentação do gcc afirma:

-Wformatestá incluído em -Wall. Para mais controle sobre alguns aspectos do formato de verificação, as opções -Wformat-y2k, -Wno-format-extra-args, -Wno-format-zero-length, -Wformat-nonliteral, -Wformat-security, e -Wformat=2estão disponíveis, mas não estão incluídos no -Wall.

O -Wformatque está habilitado na -Wallopção não habilita vários avisos especiais que ajudam a encontrar estes casos:

  • -Wformat-nonliteral avisará se você não passar uma string litteral como especificador de formato.
  • -Wformat-securityavisará se você passar uma string que pode conter uma construção perigosa. É um subconjunto de -Wformat-nonliteral.

Tenho que admitir que a ativação -Wformat-securityrevelou vários bugs que tínhamos em nossa base de código (módulo de registro, módulo de tratamento de erros, módulo de saída xml, todos tinham algumas funções que poderiam fazer coisas indefinidas se tivessem sido chamadas com% caracteres em seus parâmetros. nossa base de código agora tem cerca de 20 anos e mesmo que estivéssemos cientes desse tipo de problema, ficamos extremamente surpresos quando habilitamos esses avisos de quantos desses bugs ainda estavam na base de código).

Patrick Schlüter
fonte
1

Além das outras respostas bem explicadas com quaisquer questões secundárias abordadas, gostaria de dar uma resposta precisa e concisa à pergunta fornecida.


Por que o uso de printfum único argumento (sem especificadores de conversão) foi suspenso?

Uma printfchamada de função com um único argumento em geral não é obsoleta e também não tem vulnerabilidades quando usada corretamente, pois você sempre deve codificar.

C Usuários em todo o mundo, de iniciante a especialista em status, usam printfessa forma para fornecer uma frase de texto simples como saída para o console.

Além disso, Alguém tem que distinguir se este único argumento é uma string literal ou um ponteiro para uma string, que é válido, mas normalmente não é usado. Para o último, é claro, podem ocorrer saídas inconvenientes ou qualquer tipo de comportamento indefinido , quando o ponteiro não está definido corretamente para apontar para uma string válida, mas essas coisas também podem ocorrer se os especificadores de formato não corresponderem aos respectivos argumentos, fornecendo vários argumentos.

Obviamente, também não é correto e apropriado que a string, fornecida como um e único argumento, tenha qualquer formato ou especificadores de conversão, uma vez que não haverá conversão.

Dito isso, fornecer um literal de string simples "Hello World!"como único argumento sem nenhum especificador de formato dentro dessa string, como você forneceu na pergunta:

printf("Hello World!");

não é preterido ou " má prática " nem tem vulnerabilidades.

Na verdade, muitos programadores C começam e começaram a aprender e usar C ou mesmo linguagens de programação em geral com aquele programa HelloWorld e esta printfinstrução como os primeiros de seu tipo.

Eles não seriam isso se fossem descontinuados.

Em um livro que estou lendo, está escrito que printfcom um único argumento (sem especificadores de conversão) está obsoleto.

Bem, então eu colocaria o foco no livro ou no próprio autor. Se um autor está realmente fazendo essas afirmações , na minha opinião, incorretas e até mesmo ensinando isso sem explicar explicitamente por que está fazendo isso (se essas afirmações forem realmente literalmente equivalentes fornecidas naquele livro), eu consideraria um livro ruim . Um bom livro, ao contrário disso, deve explicar por que evitar certos tipos de métodos ou funções de programação.

De acordo com o que eu disse acima, usar printfcom apenas um argumento (uma string literal) e sem quaisquer especificadores de formato não é em nenhum caso descontinuado ou considerado como "má prática" .

Você deve perguntar ao autor o que ele quis dizer com isso ou, melhor ainda, que ele esclareça ou corrija a seção relativa para a próxima edição ou impressões em geral.

RobertS apoia Monica Cellio
fonte
Você pode adicionar que nãoprintf("Hello World!"); é equivalente a qualquer maneira, o que diz algo sobre o autor da recomendação. puts("Hello World!");
chqrlie