Mikael
Mikael

Reputation: 979

What is the underlying difference between printf(s) and printf("%s", s)?

The question is plain and simple, s is a string, I suddenly got the idea to try to use printf(s) to see if it would work and I got a warning in one case and none in the other.

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".

So what's the underlying difference between printf(s) and printf("%s", s) and why do I get a warning in just one case?

Upvotes: 28

Views: 5447

Answers (5)

Jonathan Leffler
Jonathan Leffler

Reputation: 755064

In the first case, the non-literal format string could perhaps come from user code or user-supplied (run-time) data, in which case it might contain %s or other conversion specifications, for which you've not passed the data. This can lead to all sorts of reading problems (and writing problems if the string includes %n — see printf() or your C library's manual pages).

In the second case, the format string controls the output and it doesn't matter whether any string to be printed contains conversion specifications or not (though the code shown prints an integer, not a string). The compiler (GCC or Clang is used in the question) assumes that because there are arguments after the (non-literal) format string, the programmer knows what they're up to.

The first is a 'format string' vulnerability. You can search for more information on the topic.

GCC knows that most times the single argument printf() with a non-literal format string is an invitation to trouble. You could use puts() or fputs() instead. It is sufficiently dangerous that GCC generates the warnings with the minimum of provocation.

The more general problem of a non-literal format string can also be problematic if you are not careful — but extremely useful assuming you are careful. You have to work harder to get GCC to complain: it requires both -Wformat and -Wformat-nonliteral to get the complaint.

From the comments:

So ignoring the warning, as if I really know what I am doing and there will be no errors, is one or another more efficient to use or are they the same? Considering both space and time.

Of your three printf() statements, given the tight context that the variable s is as assigned immediately above the call, there is no actual problem. But you could use puts(s) if you omitted the newline from the string or fputs(s, stdout) as it is and get the same result, without the overhead of printf() parsing the entire string to find out that it is all simple characters to be printed.

The second printf() statement is also safe as written; the format string matches the data passed. There is no significant difference between that and simply passing the format string as a literal — except that the compiler can do more checking if the format string is a literal. The run-time result is the same.

The third printf() passes more data arguments than the format string needs, but that is benign. It isn't ideal, though. Again, the compiler can check better if the format string is a literal, but the run-time effect is practically the same.

From the printf() specification linked to at the top:

Each of these functions converts, formats, and prints its arguments under control of the format. The format is a character string, beginning and ending in its initial shift state, if any. The format is composed of zero or more directives: ordinary characters, which are simply copied to the output stream, and conversion specifications, each of which shall result in the fetching of zero or more arguments. The results are undefined if there are insufficient arguments for the format. If the format is exhausted while arguments remain, the excess arguments shall be evaluated but are otherwise ignored.

In all these cases, there is no strong indication of why the format string is not a literal. However, one reason for wanting a non-literal format string might be that sometimes you print the floating point numbers in %f notation and sometimes in %e notation, and you need to choose which at run-time. (If it is simply based on value, %g might be appropriate, but there are times when you want the explicit control — always %e or always %f.)

Upvotes: 26

Scott Mermelstein
Scott Mermelstein

Reputation: 15397

There are two things in play in your question.

The first is covered succinctly by Jonathan Leffler - the warning you're getting is because the string isn't literal and doesn't have any format specifiers in it.

The other is the mystery of why the compiler doesn't issue a warning that your number of arguments doesn't match the number of specifiers. The short answer is "because it doesn't," but more specifically, printf is a variadic function. It takes any number of arguments after the initial format specification - from 0 on up. The compiler can't check to see if you gave the right amount; that's up to the printf function itself, and leads to the undefined behavior that Joachim mentioned in comments.

EDIT: I'm going to give further answer to your question, as a means of getting on a small soapbox.

What's the difference between printf(s) and printf("%s", s)? Simple - in the latter, you're using printf as it's declared. "%s" is a const char *, and it will subsequently not generate the warning message.

In your comments to other answers, you mentioned "Ignoring the warning...". Don't do this. Warnings exist for a reason, and should be resolved (otherwise they're just noise, and you'll miss warnings that actually matter among the cruft of all the ones that don't.)

Your issue can be resolved in several ways.

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

will resolve the warning, because you're now using a const pointer, and there are none of the dangers that Jonathan mentioned. (You could also declare it as const char* const s, but don't have to. The first const is important, because it then matches the declaration of printf, and because const char* s means that characters pointed to by s can't change, i.e. the string is a literal.)

Or, even simpler, just do:

printf("abcdefghij\n");

This is implicitly a const pointer, and also not a problem.

Upvotes: 5

plugwash
plugwash

Reputation: 10523

So what's the underlying difference between printf(s) and printf("%s", s)

"printf(s)" will treat s as a format string. If s contains format specifiers then printf will interpret them and go looking for varargs. Since no varargs actually exist this will likely trigger undefined behaviour.

If an attacker controls "s" then this is likely to be a security hole.

printf("%s",s) will just print what is in the string.

and why do I get a warning in just one case?

Warnings are a balance between catching dangerous stupidity and not creating too much noise.

C programmers are in the habbit of using printf and various printf like functions* as generic print functions even when they don't actually need formatting. In this environment it's easy for someone to make the mistake of writing printf(s) without thinking about where s came from. Since formatting is pretty useless without any data to format printf(s) has little legitimate use.

printf(s,format,arguments) on the other hand indicates that the programmer deliberately intended formatting to take place.

Afaict this warning is not turned on by default in upstream gcc, but some distros are turning it on as part of their efforts to reduce security holes.

* Both standard C functions like sprintf and fprintf and functions in third party libraries.

Upvotes: 3

Sourav Ghosh
Sourav Ghosh

Reputation: 134396

The warning says it all.

First, to discuss about the issue, as per the signature, the first parameter to printf() is a format string which can contain format specifiers (conversion specifier). In case, a string contains a format specifier and the corresponding argument is not supplied, it invokes undefined behavior.

So, a cleaner (or safer) approach (of printing a string which needs no format specification) would be puts(s); over printf(s); (the former does not process s for any conversion specifiers, removing the reason for the possible UB in the later case). You can choose fputs(), if you're worried about the ending newline that automatically gets added in puts().


That said, regarding the warning option, -Wformat-security from the online gcc manual

At present, this warns about calls to printf and scanf functions where the format string is not a string literal and there are no format arguments, as in printf (foo);. This may be a security hole if the format string came from untrusted input and contains %n.

In your first case, there's only one argument supplied to printf(), which is not a string literal, rather a variable, which can be very well generated/ populated at run time, and if that contains unexpected format specifiers, it may invoke UB. Compiler has no way to check for the presence of any format specifier in that. That is the security problem there.

In the second case, the accompanying argument is supplied, the format specifier is not the only argument passed to printf(), so the first argument need not to be verified. Hence the warning is not there.


Update:

Regarding the third one, with excess argument that required by the supplied format string

printf(s, 99, 50);

quoting from C11, chapter §7.21.6.1

[...] If the format is exhausted while arguments remain, the excess arguments are evaluated (as always) but are otherwise ignored. [...]

So, passing excess argument is not a problem (from the compiler perspective) at all and it is well defined. NO scope for any warning there.

Upvotes: 6

Andreas Spindler
Andreas Spindler

Reputation: 8150

The underlying reason: printf is declared like:

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

This tells gcc that printf is a function with a printf-style interface where the format string comes first. IMHO it must be literal; I don't think there's a way to tell the good compiler that s is actually a pointer to a literal string it had seen before.

Read more about __attribute__ here.

Upvotes: 2

Related Questions