¿Cuál es la diferencia subyacente entre printf(s) y printf("% s", s)?




string compiler-warnings (4)

La pregunta es simple y simple, s es una cadena, de repente se me ocurrió la idea de intentar usar printf(s) para ver si funcionaba y recibí una advertencia en un caso y ninguna en el otro.

char* s = "abcdefghij\n";
printf(s);

// Warning raised with gcc -std=c11: 
// format not a string literal and no format arguments [-Wformat-security]

// On the other hand, if I use 

char* s = "abc %d efg\n";
printf(s, 99);

// I get no warning whatsoever, why is that?

// Update, I've tested this:
char* s = "random %d string\n";
printf(s, 99, 50);

// Results: no warning, output "random 99 string".

Entonces, ¿cuál es la diferencia subyacente entre printf(s) y printf("%s", s) y por qué recibo una advertencia en un solo caso?


Entonces, ¿cuál es la diferencia subyacente entre printf (s) y printf ("% s", s)

"printf (s)" tratará s como una cadena de formato. Si s contiene especificadores de formato, printf los interpretará y buscará varargs. Dado que en realidad no existen varargs, es probable que se desencadene un comportamiento indefinido.

Si un atacante controla "s", es probable que esto sea un agujero de seguridad.

printf ("% s", s) solo imprimirá lo que está en la cadena.

¿Y por qué recibo una advertencia en un solo caso?

Las advertencias son un equilibrio entre la captura de estupidez peligrosa y no crear demasiado ruido.

Los programadores de C están en el hábito de usar printf y varias funciones similares a printf * como funciones de impresión genéricas, incluso cuando en realidad no necesitan formato. En este entorno, es fácil para alguien cometer el error de escribir printf (s) sin pensar en el origen. Dado que el formateo es bastante inútil sin datos para formatear printf (s) tiene poco uso legítimo.

printf (s, formato, argumentos), por otro lado, indica que el programador intentó deliberadamente que el formateo tuviera lugar.

Afaict esta advertencia no está activada de forma predeterminada en gcc en sentido ascendente, pero algunas distros la están activando como parte de sus esfuerzos para reducir los agujeros de seguridad.

* Ambas funciones estándar de C como sprintf y fprintf y funciones en bibliotecas de terceros.


En el primer caso, la cadena de formato no literal podría provenir del código de usuario o los datos proporcionados por el usuario (tiempo de ejecución), en cuyo caso podría contener %s u otras especificaciones de conversión, para las cuales no ha pasado los datos. . Esto puede llevar a todo tipo de problemas de lectura (y problemas de escritura si la cadena incluye %n , vea printf() o las páginas del manual de su biblioteca de C).

En el segundo caso, la cadena de formato controla la salida y no importa si alguna cadena que se imprima contiene especificaciones de conversión o no (aunque el código que se muestra imprime un número entero, no una cadena). El compilador (GCC o Clang se usa en la pregunta) asume que debido a que hay argumentos después de la cadena de formato (no literal), el programador sabe lo que están haciendo.

El primero es una vulnerabilidad de 'cadena de formato'. Puede buscar más información sobre el tema.

GCC sabe que la mayoría de las veces el único argumento printf() con una cadena de formato no literal es una invitación a problemas. Podrías usar puts() o fputs() lugar. Es suficientemente peligroso que GCC genere las advertencias con el mínimo de provocación.

El problema más general de una cadena de formato no literal también puede ser problemático si no tiene cuidado, pero es extremadamente útil suponiendo que sea cuidadoso. Tienes que trabajar más duro para que GCC se queje: requiere tanto -Wformat como -Wformat-nonliteral para recibir la queja.

De los comentarios:

Entonces, ignorando la advertencia, como si realmente supiera lo que estoy haciendo y no habrá errores, ¿es uno u otro más eficiente de usar o son los mismos? Teniendo en cuenta tanto el espacio como el tiempo.

De sus tres declaraciones printf() , dado el contexto estrecho de que la variable s está asignada inmediatamente por encima de la llamada, no hay problema real. Sin embargo, podría usar el puts(s) fputs(s, stdout) puts(s) si omitió la nueva línea de la cadena o fputs(s, stdout) como está y obtiene el mismo resultado, sin la sobrecarga de printf() analizando toda la cadena para descubrir que es todo Caracteres sencillos para imprimir.

La segunda declaración printf() también es segura como está escrita; La cadena de formato coincide con los datos pasados. No hay una diferencia significativa entre eso y simplemente pasar la cadena de formato como un literal, excepto que el compilador puede hacer más comprobaciones si la cadena de formato es un literal. El resultado en tiempo de ejecución es el mismo.

El tercer printf() pasa más argumentos de datos que los que necesita la cadena de formato, pero eso es benigno. Aunque no es ideal. Nuevamente, el compilador puede verificar mejor si la cadena de formato es un literal, pero el efecto en tiempo de ejecución es prácticamente el mismo.

Desde la especificación de printf() enlazada en la parte superior:

Cada una de estas funciones convierte, formatea e imprime sus argumentos bajo el control del formato . El formato es una cadena de caracteres, que comienza y termina en su estado de cambio inicial, si corresponde. El formato se compone de cero o más directivas: caracteres ordinarios, que simplemente se copian al flujo de salida, y especificaciones de conversión, cada una de las cuales dará como resultado la obtención de cero o más argumentos. Los resultados no están definidos si no hay argumentos suficientes para el formato. Si el formato se agota mientras permanecen los argumentos, los argumentos en exceso se evaluarán, pero de lo contrario se ignorarán.

En todos estos casos, no hay una indicación clara de por qué la cadena de formato no es un literal. Sin embargo, una razón para querer una cadena de formato no literal podría ser que a veces imprime los números de punto flotante en notación %f y otras veces en notación %e , y debe elegir cuál en tiempo de ejecución. (Si se basa simplemente en el valor, %g puede ser apropiado, pero hay ocasiones en las que desea el control explícito, siempre %e o siempre %f ).


La advertencia lo dice todo.

Primero, para discutir sobre el problema , según la firma, el primer parámetro para printf() es una cadena de formato que puede contener especificadores de formato ( especificador de conversión ). En el caso, una cadena contiene un especificador de formato y el argumento correspondiente no se proporciona, invoca un comportamiento indefinido .

Por lo tanto, un enfoque más limpio ( o más seguro ) (de imprimir una cadena que no necesita una especificación de formato) sería puts(s); sobre printf(s); ( el primero no procesa s para ningún especificador de conversión, eliminando el motivo de la posible UB en el último caso ). Puede elegir fputs() , si le preocupa la nueva línea de finalización que se agrega automáticamente a puts() .

Dicho esto, con respecto a la opción de advertencia, -Wformat-security del manual en línea de gcc

Actualmente, esto advierte sobre las llamadas a las funciones printf y scanf donde la cadena de formato no es una cadena literal y no hay argumentos de formato, como en printf (foo); . Esto puede ser un agujero de seguridad si la cadena de formato proviene de una entrada no confiable y contiene %n .

En su primer caso, solo se proporciona un argumento a printf() , que no es una cadena literal, sino una variable , que se puede generar / rellenar muy bien en el tiempo de ejecución, y si contiene especificadores de formato inesperados , puede invocar UB . El compilador no tiene forma de verificar la presencia de ningún especificador de formato en eso. Ese es el problema de seguridad allí.

En el segundo caso, se proporciona el argumento adjunto, el especificador de formato no es el único argumento pasado a printf() , por lo que el primer argumento no necesita ser verificado. Por lo tanto, la advertencia no está allí.

Actualizar:

Con respecto al tercero, con el argumento en exceso que requiere la cadena de formato suministrada

printf(s, 99, 50);

citando de C11 , capítulo §7.21.6.1

[...] Si el formato se agota mientras permanecen los argumentos, los argumentos en exceso se evalúan (como siempre), pero de lo contrario se ignoran. [...]

Por lo tanto, pasar el argumento en exceso no es un problema (desde la perspectiva del compilador) y está bien definido. NO hay margen para cualquier advertencia allí.


La razón subyacente: printf se declara como:

int printf(const char *fmt, ...) __attribute__ ((format(printf, 1, 2)));

Esto le dice a gcc que printf es una función con una interfaz de estilo printf donde la cadena de formato es lo primero. En mi humilde opinión debe ser literal; No creo que haya una manera de decirle al buen compilador que s es en realidad un puntero a una cadena literal que había visto antes.

Lea más sobre __attribute__ here .







format-specifiers